2011-02-01 09:57:29 +08:00
#
2015-03-18 03:21:11 +08:00
# Copyright (C) 2011 - 2015 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'
2014-02-14 06:12:38 +08:00
require 'csv'
2011-02-01 09:57:29 +08:00
class Course < ActiveRecord :: Base
include Context
include Workflow
2012-12-27 08:51:39 +08:00
include TextHelper
2014-02-05 04:53:04 +08:00
include HtmlTextHelper
2014-05-13 06:57:33 +08:00
include TimeZoneHelper
2014-11-14 02:35:24 +08:00
include ContentLicenses
2015-06-19 03:20:23 +08:00
include TurnitinID
2011-02-01 09:57:29 +08:00
2014-09-26 05:38:07 +08:00
attr_accessor :teacher_names
2014-11-17 22:52:50 +08:00
attr_writer :student_count , :primary_enrollment_type , :primary_enrollment_role_id , :primary_enrollment_rank , :primary_enrollment_state , :invitation
2014-09-26 05:38:07 +08:00
2011-06-08 00:31:14 +08:00
attr_accessible :name ,
:section ,
:account ,
:group_weighting_scheme ,
:start_at ,
:conclude_at ,
:grading_standard_id ,
:is_public ,
2015-01-09 01:35:09 +08:00
:is_public_to_auth_users ,
2011-06-08 00:31:14 +08:00
:allow_student_wiki_edits ,
:show_public_context_messages ,
:syllabus_body ,
2012-03-31 00:36:17 +08:00
:public_description ,
2011-06-08 00:31:14 +08:00
:allow_student_forum_attachments ,
2013-01-19 06:36:50 +08:00
:allow_student_discussion_topics ,
2013-02-08 04:09:05 +08:00
:allow_student_discussion_editing ,
allow displaing total grade as points for students
fixes CNVS-9874
when teacher choses to show total grade as points in GB2, a setting is saved
student grade summary page shows the total grade in same format
if assignment groups are weighted, grade is displayed as a percentage again
test plan:
- new addition:
- go to a course where GB2 is displaying totals as points BUT has no DB setting about show_point_totals
(ask mike if you need help getting to this state)
- as a student look at the grade summary page, total should be a percent
- as a teacher in that class, go to GB2 and wait for 5 seconds
- as that same student, go back to grade summary, total should be points now
- as a teacher, change total grade to show as points in GB2
- when you return to the GB, even from a new browser, this should be the same
- in GB, tooltip should show points if cell shows percent, and visa vera
- as a student in that class, you should see your total grade as points
- grade tooltip should show points if cell shows percent, and visa vera
- switch to weighted assignment groups
- total grade as a teacher and student should switch back to percent
- switch back to non-weighted
- total grade should stay a percent until explicitly changed back to points
- GB2 weighting and points/percent switching should have no unintended effects on the GB
- student grade summary page should display everything in a consistent manner
Change-Id: Id0c4f496576c226eb7000d6684a37faf0b439359
Reviewed-on: https://gerrit.instructure.com/28780
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Caleb Guanzon <cguanzon@instructure.com>
Reviewed-by: Simon Williams <simon@instructure.com>
Product-Review: Mike Nomitch <mnomitch@instructure.com>
2014-01-15 01:06:03 +08:00
:show_total_grade_as_points ,
2011-06-08 00:31:14 +08:00
:default_wiki_editing_roles ,
:allow_student_organized_groups ,
:course_code ,
:default_view ,
:show_all_discussion_entries ,
:open_enrollment ,
:allow_wiki_comments ,
:turnitin_comments ,
:self_enrollment ,
:license ,
:indexed ,
:enrollment_term ,
:abstract_course ,
:root_account ,
:storage_quota ,
:storage_quota_mb ,
:restrict_enrollments_to_course_dates ,
2015-03-07 07:17:57 +08:00
:restrict_student_past_view ,
:restrict_student_future_view ,
2011-06-08 00:31:14 +08:00
:grading_standard ,
2011-07-13 04:31:40 +08:00
:grading_standard_enabled ,
2011-08-11 01:48:34 +08:00
:locale ,
2014-01-15 14:48:27 +08:00
:integration_id ,
2012-09-06 05:57:33 +08:00
:hide_final_grades ,
2013-02-05 05:53:31 +08:00
:hide_distribution_graphs ,
2013-04-06 04:40:05 +08:00
:lock_all_announcements ,
2014-05-13 06:57:33 +08:00
:public_syllabus ,
2014-09-30 21:13:32 +08:00
:course_format ,
2015-07-21 05:19:44 +08:00
:time_zone ,
:organize_epub_by_content_type
2011-02-01 09:57:29 +08:00
2014-05-13 06:57:33 +08:00
time_zone_attribute :time_zone
2016-02-04 05:50:28 +08:00
def time_zone
2014-05-13 06:57:33 +08:00
if read_attribute ( :time_zone )
2016-02-04 05:50:28 +08:00
super
2014-05-13 06:57:33 +08:00
else
root_account . default_time_zone
end
end
2011-02-01 09:57:29 +08:00
serialize :tab_configuration
2011-08-11 01:48:34 +08:00
serialize :settings , Hash
2011-02-01 09:57:29 +08:00
belongs_to :root_account , :class_name = > 'Account'
2011-06-08 00:31:14 +08:00
belongs_to :abstract_course
2011-02-01 09:57:29 +08:00
belongs_to :enrollment_term
2011-04-09 06:45:38 +08:00
belongs_to :grading_standard
2011-05-25 03:22:12 +08:00
belongs_to :template_course , :class_name = > 'Course'
has_many :templated_courses , :class_name = > 'Course' , :foreign_key = > 'template_course_id'
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
has_many :course_sections
2015-11-20 08:14:32 +08:00
has_many :active_course_sections , - > { where ( workflow_state : 'active' ) } , class_name : 'CourseSection'
has_many :enrollments , - > { preload ( :user ) . where ( " enrollments.workflow_state<>'deleted' " ) } , dependent : :destroy , inverse_of : :course
2014-02-28 01:46:02 +08:00
has_many :all_enrollments , :class_name = > 'Enrollment'
2015-11-20 08:14:32 +08:00
has_many :current_enrollments , - > { where ( " enrollments.workflow_state NOT IN ('rejected', 'completed', 'deleted', 'inactive') " ) . preload ( :user ) } , class_name : 'Enrollment'
has_many :all_current_enrollments , - > { where ( " enrollments.workflow_state NOT IN ('rejected', 'completed', 'deleted') " ) . preload ( :user ) } , class_name : 'Enrollment'
has_many :typical_current_enrollments , - > { where ( " enrollments.workflow_state NOT IN ('rejected', 'completed', 'deleted', 'inactive') AND enrollments.type NOT IN ('StudentViewEnrollment', 'ObserverEnrollment', 'DesignerEnrollment') " ) . preload ( :user ) } , class_name : 'Enrollment'
has_many :prior_enrollments , - > { preload ( :user , :course ) . where ( workflow_state : 'completed' ) } , class_name : 'Enrollment'
2012-08-24 05:10:00 +08:00
has_many :prior_users , :through = > :prior_enrollments , :source = > :user
2012-05-03 03:43:00 +08:00
has_many :students , :through = > :student_enrollments , :source = > :user
2016-02-19 02:09:18 +08:00
has_many :admin_visible_students , :through = > :admin_visible_student_enrollments , :source = > :user
2015-11-20 08:14:32 +08:00
has_many :self_enrolled_students , - > { where ( " self_enrolled " ) } , through : :student_enrollments , source : :user
2012-05-03 03:43:00 +08:00
has_many :all_students , :through = > :all_student_enrollments , :source = > :user
2015-11-20 08:14:32 +08:00
has_many :participating_students , - > { where ( enrollments : { type : [ 'StudentEnrollment' , 'StudentViewEnrollment' ] , workflow_state : 'active' } ) } , through : :enrollments , source : :user
has_many :student_enrollments , - > { where ( " enrollments.workflow_state NOT IN ('rejected', 'completed', 'deleted', 'inactive') AND enrollments.type IN ('StudentEnrollment', 'StudentViewEnrollment') " ) . preload ( :user ) } , class_name : 'Enrollment'
has_many :admin_visible_student_enrollments , - > { where ( " enrollments.workflow_state NOT IN ('rejected', 'completed', 'deleted') AND enrollments.type IN ('StudentEnrollment', 'StudentViewEnrollment') " ) . preload ( :user ) } , class_name : 'Enrollment'
has_many :all_student_enrollments , - > { where ( " enrollments.workflow_state<>'deleted' AND enrollments.type IN ('StudentEnrollment', 'StudentViewEnrollment') " ) . preload ( :user ) } , class_name : 'Enrollment'
2012-06-29 01:10:09 +08:00
has_many :all_real_users , :through = > :all_real_enrollments , :source = > :user
2015-11-20 08:14:32 +08:00
has_many :all_real_enrollments , - > { where ( " enrollments.workflow_state<>'deleted' AND enrollments.type<>'StudentViewEnrollment' " ) . preload ( :user ) } , class_name : 'Enrollment'
2012-05-03 03:43:00 +08:00
has_many :all_real_students , :through = > :all_real_student_enrollments , :source = > :user
2015-11-20 08:14:32 +08:00
has_many :all_real_student_enrollments , - > { where ( " enrollments.type = 'StudentEnrollment' AND enrollments.workflow_state <> 'deleted' " ) . preload ( :user ) } , class_name : 'StudentEnrollment'
2011-02-01 09:57:29 +08:00
has_many :teachers , :through = > :teacher_enrollments , :source = > :user
2015-11-20 08:14:32 +08:00
has_many :teacher_enrollments , - > { where ( " enrollments.workflow_state <> 'deleted' AND enrollments.type = 'TeacherEnrollment' " ) . preload ( :user ) } , class_name : 'TeacherEnrollment'
2011-02-01 09:57:29 +08:00
has_many :tas , :through = > :ta_enrollments , :source = > :user
2015-11-20 08:14:32 +08:00
has_many :ta_enrollments , - > { where ( " enrollments.workflow_state<>'deleted' " ) . preload ( :user ) } , class_name : 'TaEnrollment'
2011-02-01 09:57:29 +08:00
has_many :observers , :through = > :observer_enrollments , :source = > :user
2015-11-20 08:14:32 +08:00
has_many :participating_observers , - > { where ( enrollments : { workflow_state : 'active' } ) } , through : :observer_enrollments , source : :user
has_many :observer_enrollments , - > { where ( " enrollments.workflow_state<>'deleted' " ) . preload ( :user ) } , class_name : 'ObserverEnrollment'
has_many :instructors , - > { where ( enrollments : { type : [ 'TaEnrollment' , 'TeacherEnrollment' ] } ) } , through : :enrollments , source : :user
has_many :instructor_enrollments , - > { where ( type : [ 'TaEnrollment' , 'TeacherEnrollment' ] ) } , class_name : 'Enrollment'
has_many :participating_instructors , - > { where ( enrollments : { type : [ 'TaEnrollment' , 'TeacherEnrollment' ] , workflow_state : 'active' } ) } , through : :enrollments , source : :user
has_many :admins , - > { where ( enrollments : { type : [ 'TaEnrollment' , 'TeacherEnrollment' , 'DesignerEnrollment' ] } ) } , through : :enrollments , source : :user
has_many :admin_enrollments , - > { where ( type : [ 'TaEnrollment' , 'TeacherEnrollment' , 'DesignerEnrollment' ] ) } , class_name : 'Enrollment'
has_many :participating_admins , - > { where ( enrollments : { type : [ 'TaEnrollment' , 'TeacherEnrollment' , 'DesignerEnrollment' ] , workflow_state : 'active' } ) } , through : :enrollments , source : :user
2012-03-14 04:08:19 +08:00
has_many :student_view_students , :through = > :student_view_enrollments , :source = > :user
2015-11-20 08:14:32 +08:00
has_many :student_view_enrollments , - > { where ( " enrollments.workflow_state<>'deleted' " ) . preload ( :user ) } , class_name : 'StudentViewEnrollment'
2012-07-07 06:57:15 +08:00
has_many :participating_typical_users , :through = > :typical_current_enrollments , :source = > :user
2015-11-20 08:14:32 +08:00
has_many :custom_gradebook_columns , - > { order ( 'custom_gradebook_columns.position, custom_gradebook_columns.title' ) } , dependent : :destroy
2011-07-26 00:12:55 +08:00
learning outcomes refactor
This list is *NOT* complete, some items may have snuck in that I forgot
to note, and/or some of the noted items may not be completely functional
yet.
Specs need to be written around a lot of this, other specs will no doubt
need to be fixed.
Some things, particularly around LearningOutcomeGroups will need data
migrations that aren't there yet.
* remove LearningOutcome.non_rubric_outcomes? and replace with false
where invoked
* remove LearningOutcome.enabled? and replace with true where invoked
* remove never-taken branches
* remove the shared/aligned_outcomes partial and it's supporting
javascript, since it's now empty
* remove js handler for add_outcome_alignment_link and supporting
method since it only occurred in never-taken branches
* mix LearningOutcomeContext into Course and Account
* replace LearningOutcomeGroup.default_for(context) with
LearningOutcomeContext#root_outcome_group
* rename LearningOutcome#content_tags to LearningOutcome#alignments
* rename LearningOutcomeGroup#content_tags to
LearningOutcomeGroup#child_links, and properly restrict
* remove ContentTag[Alignment]#rubric_association_id, add
ContentTag[Alignment]#has_rubric_association? that looks at the
presence of the content's rubric_association_id
* condition off the assignment having a rubric_association rather than
filtering tags by has_rubric_association (which just looks back at
the assignment). all or none of the assignment's alignments are
forced to have the association (via the assignment). this was true in
practice before, is now codified (and more efficient)
* rename AssessmentQuestionBank#learning_outcome_tags to
AssessmentQuestionBank#learning_outcome_alignments
* rename Assignment#learning_outcome_tags to
Assignment#learning_outcome_alignments
* rename Rubric#learning_outcome_tags to
Rubric#learning_outcome_alignments
* move/rename (Course|Account)#learning_outcome_tags to
LearningOutcomeContext#learning_outcome_links
* move/rename Account#learning_outcomes (corrected) and
Course#learning_outcomes to
LearningOutcomeContext#linked_learning_outcomes
* move/rename Account#created_learning_outcomes and
Course#created_learning_outcomes to
LearningOutcomeContext#created_learning_outcomes
* clarify and correct usage of linked_learning_outcomes vs.
created_learning_outcomes
* move/rename (Account|Account)#learning_outcome_groups to
LearningOutcomeContext#learning_outcome_groups
* remove unused Account#associated_learning_outcomes
* just remove one link to a learning outcome when deleting
* merge Account#has_outcomes?, Course#has_outcomes? and
Course#has_outcomes into LearningOutcomeContext#has_outcomes?, add a
use in Context#active_record_types
* kill LearningOutcomeGroup#root_learning_outcome_group (unused)
* rename LearningOutcomeResult#content_tag to
LearningOutcomeResult#alignment
* kill unused (and broken) OutcomesController#add_outcome_group
* kill unused OutcomesController#update_outcomes_for_asset
* kill unused OutcomesController#outcomes_for_asset
* remove unused (outside specs, correct specs)
AssessmentQuestionBank#outcomes=
* remove unused ContentTag#learning_outcome_content
* replace ContentTag.learning_outcome_tags_for(asset) (only ever called
with asset=an assignment) with call to
Assignment#learning_outcome_alignments
* remove unused ContentTag.not_rubric
* remove (now) unused ContentTag.include_outcome
* remove unused LearningOutcome#learning_outcome_group_associations
* avoid explicit use of ContentTag in outcome-related specs
* replace LearningOutcomeGroup#learning_outcome_tags with
LearningOutcomeGroup#child_outcome_links (and only use for outcome
links; not tags for child groups)
* split ContentTag#create_outcome_result into
Submission#create_outcome_result,
QuizSubmission#create_outcome_result, and
RubricAssessment#create_outcome_result. fix some bugs along the way
* refactor ContentTag.outcome_tags_for_banks and some code from
QuizSubmission#(track_outcomes|update_outcomes_for_assessment_questions)
into QuizSubmission#questions_and_alignments
* refactor RubricAssociation#update_outcome_relations and
Rubric#update_alignments into LearningOutcome.update_alignments
* don't use ContentTag#rubric_association with outcome alignments; use
the tag's content's rubric_association in its place (they should have
been equal anyways)
* refactor LearningOutcome.available_in_context and
@context.root_outcome_group.sorted_all_outcomes (only time
sorted_all_outcomes is used) into
LearningOutcomeContext#available_outcomes and
LearningOutcomeContext#available_outcome
* overhaul LearningOutcomeGroup#sorted_content and rename to
LearningOutcomeGroup#sorted_children. it not returns ContentTags
(outcome links) and LearningOutcomeGroups, vs. LearningOutcomes and
LearningOutcomeGroups; fix usages appropriately
* fix UI for arranging/deleting outcome links and groups within a group
to refer to the outcome link rather than the outcome
Change-Id: I85d99f2634f7206332cb1f5d5ea575b428988d4b
Reviewed-on: https://gerrit.instructure.com/12590
Reviewed-by: Jacob Fugal <jacob@instructure.com>
Tested-by: Jacob Fugal <jacob@instructure.com>
2012-07-13 01:16:13 +08:00
include LearningOutcomeContext
2013-06-18 05:12:49 +08:00
include RubricContext
learning outcomes refactor
This list is *NOT* complete, some items may have snuck in that I forgot
to note, and/or some of the noted items may not be completely functional
yet.
Specs need to be written around a lot of this, other specs will no doubt
need to be fixed.
Some things, particularly around LearningOutcomeGroups will need data
migrations that aren't there yet.
* remove LearningOutcome.non_rubric_outcomes? and replace with false
where invoked
* remove LearningOutcome.enabled? and replace with true where invoked
* remove never-taken branches
* remove the shared/aligned_outcomes partial and it's supporting
javascript, since it's now empty
* remove js handler for add_outcome_alignment_link and supporting
method since it only occurred in never-taken branches
* mix LearningOutcomeContext into Course and Account
* replace LearningOutcomeGroup.default_for(context) with
LearningOutcomeContext#root_outcome_group
* rename LearningOutcome#content_tags to LearningOutcome#alignments
* rename LearningOutcomeGroup#content_tags to
LearningOutcomeGroup#child_links, and properly restrict
* remove ContentTag[Alignment]#rubric_association_id, add
ContentTag[Alignment]#has_rubric_association? that looks at the
presence of the content's rubric_association_id
* condition off the assignment having a rubric_association rather than
filtering tags by has_rubric_association (which just looks back at
the assignment). all or none of the assignment's alignments are
forced to have the association (via the assignment). this was true in
practice before, is now codified (and more efficient)
* rename AssessmentQuestionBank#learning_outcome_tags to
AssessmentQuestionBank#learning_outcome_alignments
* rename Assignment#learning_outcome_tags to
Assignment#learning_outcome_alignments
* rename Rubric#learning_outcome_tags to
Rubric#learning_outcome_alignments
* move/rename (Course|Account)#learning_outcome_tags to
LearningOutcomeContext#learning_outcome_links
* move/rename Account#learning_outcomes (corrected) and
Course#learning_outcomes to
LearningOutcomeContext#linked_learning_outcomes
* move/rename Account#created_learning_outcomes and
Course#created_learning_outcomes to
LearningOutcomeContext#created_learning_outcomes
* clarify and correct usage of linked_learning_outcomes vs.
created_learning_outcomes
* move/rename (Account|Account)#learning_outcome_groups to
LearningOutcomeContext#learning_outcome_groups
* remove unused Account#associated_learning_outcomes
* just remove one link to a learning outcome when deleting
* merge Account#has_outcomes?, Course#has_outcomes? and
Course#has_outcomes into LearningOutcomeContext#has_outcomes?, add a
use in Context#active_record_types
* kill LearningOutcomeGroup#root_learning_outcome_group (unused)
* rename LearningOutcomeResult#content_tag to
LearningOutcomeResult#alignment
* kill unused (and broken) OutcomesController#add_outcome_group
* kill unused OutcomesController#update_outcomes_for_asset
* kill unused OutcomesController#outcomes_for_asset
* remove unused (outside specs, correct specs)
AssessmentQuestionBank#outcomes=
* remove unused ContentTag#learning_outcome_content
* replace ContentTag.learning_outcome_tags_for(asset) (only ever called
with asset=an assignment) with call to
Assignment#learning_outcome_alignments
* remove unused ContentTag.not_rubric
* remove (now) unused ContentTag.include_outcome
* remove unused LearningOutcome#learning_outcome_group_associations
* avoid explicit use of ContentTag in outcome-related specs
* replace LearningOutcomeGroup#learning_outcome_tags with
LearningOutcomeGroup#child_outcome_links (and only use for outcome
links; not tags for child groups)
* split ContentTag#create_outcome_result into
Submission#create_outcome_result,
QuizSubmission#create_outcome_result, and
RubricAssessment#create_outcome_result. fix some bugs along the way
* refactor ContentTag.outcome_tags_for_banks and some code from
QuizSubmission#(track_outcomes|update_outcomes_for_assessment_questions)
into QuizSubmission#questions_and_alignments
* refactor RubricAssociation#update_outcome_relations and
Rubric#update_alignments into LearningOutcome.update_alignments
* don't use ContentTag#rubric_association with outcome alignments; use
the tag's content's rubric_association in its place (they should have
been equal anyways)
* refactor LearningOutcome.available_in_context and
@context.root_outcome_group.sorted_all_outcomes (only time
sorted_all_outcomes is used) into
LearningOutcomeContext#available_outcomes and
LearningOutcomeContext#available_outcome
* overhaul LearningOutcomeGroup#sorted_content and rename to
LearningOutcomeGroup#sorted_children. it not returns ContentTags
(outcome links) and LearningOutcomeGroups, vs. LearningOutcomes and
LearningOutcomeGroups; fix usages appropriately
* fix UI for arranging/deleting outcome links and groups within a group
to refer to the outcome link rather than the outcome
Change-Id: I85d99f2634f7206332cb1f5d5ea575b428988d4b
Reviewed-on: https://gerrit.instructure.com/12590
Reviewed-by: Jacob Fugal <jacob@instructure.com>
Tested-by: Jacob Fugal <jacob@instructure.com>
2012-07-13 01:16:13 +08:00
2011-02-01 09:57:29 +08:00
has_many :course_account_associations
2015-11-20 08:14:32 +08:00
has_many :non_unique_associated_accounts , - > { order ( 'course_account_associations.depth' ) } , source : :account , through : :course_account_associations
has_many :users , - > { uniq } , through : :enrollments , source : :user
has_many :current_users , - > { uniq } , through : :current_enrollments , source : :user
has_many :all_current_users , - > { uniq } , through : :all_current_enrollments , source : :user
has_many :group_categories , - > { where ( deleted_at : nil ) } , as : :context
2011-09-22 04:31:36 +08:00
has_many :all_group_categories , :class_name = > 'GroupCategory' , :as = > :context
2011-02-01 09:57:29 +08:00
has_many :groups , :as = > :context
2015-11-20 08:14:32 +08:00
has_many :active_groups , - > { where ( " groups.workflow_state<>'deleted' " ) } , as : :context , class_name : 'Group'
has_many :assignment_groups , - > { order ( 'assignment_groups.position, assignment_groups.name' ) } , as : :context , dependent : :destroy
has_many :assignments , - > { order ( 'assignments.created_at' ) } , as : :context , dependent : :destroy
has_many :calendar_events , - > { where ( " calendar_events.workflow_state<>'cancelled' " ) } , as : :context , dependent : :destroy
has_many :submissions , - > { order ( 'submissions.updated_at DESC' ) } , through : :assignments , dependent : :destroy
2015-02-05 00:54:50 +08:00
has_many :submission_comments , as : :context
2015-11-20 08:14:32 +08:00
has_many :discussion_topics , - > { where ( " discussion_topics.workflow_state<>'deleted' " ) . preload ( :user ) . order ( 'discussion_topics.position DESC, discussion_topics.created_at DESC' ) } , as : :context , dependent : :destroy
has_many :active_discussion_topics , - > { where ( " discussion_topics.workflow_state<>'deleted' " ) . preload ( :user ) } , as : :context , class_name : 'DiscussionTopic'
has_many :all_discussion_topics , - > { preload ( :user ) } , as : :context , class_name : " DiscussionTopic " , dependent : :destroy
has_many :discussion_entries , - > { preload ( :discussion_topic , :user ) } , through : :discussion_topics , dependent : :destroy
2011-02-01 09:57:29 +08:00
has_many :announcements , :as = > :context , :class_name = > 'Announcement' , :dependent = > :destroy
2015-11-20 08:14:32 +08:00
has_many :active_announcements , - > { where ( " discussion_topics.workflow_state<>'deleted' " ) } , as : :context , class_name : 'Announcement'
2011-09-29 07:26:18 +08:00
has_many :attachments , :as = > :context , :dependent = > :destroy , :extend = > Attachment :: FindInContextAssociation
2015-11-20 08:14:32 +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 , class_name : 'Attachment'
has_many :active_assignments , - > { where ( " assignments.workflow_state<>'deleted' " ) . order ( 'assignments.title, assignments.position' ) } , as : :context , class_name : 'Assignment'
has_many :folders , - > { order ( 'folders.name' ) } , as : :context , dependent : :destroy
has_many :active_folders , - > { where ( " folders.workflow_state<>'deleted' " ) . order ( 'folders.name' ) } , class_name : 'Folder' , as : :context
2011-02-01 09:57:29 +08:00
has_many :messages , :as = > :context , :dependent = > :destroy
2015-11-20 08:14:32 +08:00
has_many :context_external_tools , - > { order ( 'name' ) } , as : :context , dependent : :destroy
2011-02-01 09:57:29 +08:00
belongs_to :wiki
2016-02-17 05:51:39 +08:00
has_many :wiki_pages , foreign_key : 'wiki_id' , primary_key : 'wiki_id'
2015-11-20 08:14:32 +08:00
has_many :quizzes , - > { order ( 'lock_at, title, id' ) } , class_name : 'Quizzes::Quiz' , as : :context , dependent : :destroy
2015-06-04 21:51:57 +08:00
has_many :quiz_questions , :class_name = > 'Quizzes::QuizQuestion' , :through = > :quizzes
2015-11-20 08:14:32 +08:00
has_many :active_quizzes , - > { preload ( :assignment ) . where ( " quizzes.workflow_state<>'deleted' " ) . order ( :created_at ) } , class_name : 'Quizzes::Quiz' , as : :context
2011-08-27 08:33:01 +08:00
has_many :assessment_questions , :through = > :assessment_question_banks
2015-11-20 08:14:32 +08:00
has_many :assessment_question_banks , - > { preload ( :assessment_questions , :assessment_question_bank_users ) } , as : :context
2011-08-27 08:33:01 +08:00
def inherited_assessment_question_banks ( include_self = false )
self . account . inherited_assessment_question_banks ( true , * ( include_self ? [ self ] : [ ] ) )
end
2011-02-01 09:57:29 +08:00
has_many :external_feeds , :as = > :context , :dependent = > :destroy
belongs_to :default_grading_standard , :class_name = > 'GradingStandard' , :foreign_key = > 'grading_standard_id'
2015-11-20 08:14:32 +08:00
has_many :grading_standards , - > { where ( " workflow_state<>'deleted' " ) } , as : :context
has_many :web_conferences , - > { order ( 'created_at DESC' ) } , as : :context , dependent : :destroy
has_many :collaborations , - > { order ( 'title, created_at' ) } , as : :context , dependent : :destroy
has_many :context_modules , - > { order ( :position ) } , as : :context , dependent : :destroy
2015-09-01 06:59:03 +08:00
has_many :context_module_progressions , through : :context_modules
2015-11-20 08:14:32 +08:00
has_many :active_context_modules , - > { where ( workflow_state : 'active' ) } , as : :context , class_name : 'ContextModule'
has_many :context_module_tags , - > { order ( :position ) . where ( tag_type : 'context_module' ) } , class_name : 'ContentTag' , as : 'context' , dependent : :destroy
2011-02-01 09:57:29 +08:00
has_many :media_objects , :as = > :context
has_many :page_views , :as = > :context
2012-09-11 02:28:03 +08:00
has_many :asset_user_accesses , :as = > :context
2011-02-01 09:57:29 +08:00
has_many :role_overrides , :as = > :context
2014-05-05 20:23:55 +08:00
has_many :content_migrations , :as = > :context
2014-06-02 23:24:19 +08:00
has_many :content_exports , :as = > :context
2015-11-20 08:14:32 +08:00
has_many :epub_exports , - > { order ( " created_at DESC " ) }
2015-09-17 07:05:30 +08:00
attr_accessor :latest_epub_export
2015-11-20 08:14:32 +08:00
has_many :alerts , - > { preload ( :criteria ) } , as : :context
2012-04-19 07:47:03 +08:00
has_many :appointment_group_contexts , :as = > :context
has_many :appointment_groups , :through = > :appointment_group_contexts
2015-11-20 08:14:32 +08:00
has_many :appointment_participants , - > { where ( " workflow_state = 'locked' AND parent_calendar_event_id IS NOT NULL " ) } , class_name : 'CalendarEvent' , foreign_key : :effective_context_code , primary_key : :asset_string
2011-02-01 09:57:29 +08:00
attr_accessor :import_source
2011-12-10 06:37:12 +08:00
has_many :zip_file_imports , :as = > :context
2012-09-26 00:44:35 +08:00
has_many :content_participation_counts , :as = > :context , :dependent = > :destroy
2014-06-04 05:52:30 +08:00
has_many :poll_sessions , class_name : 'Polling::PollSession' , dependent : :destroy
add grading period group model
add GradingPeriodGroup, and change associations between GradingPeriods,
GradingPeriodGroups, Courses, and Accounts. also adjust the grading
periods controller to account for addition of grading period groups
closes CNVS-16538
test plan:
-run bundle exec rake db:migrate, and bundle exec rake db:migrate RAILS_ENV=test
-verify the migrations successfully run
-open the rails console in sandbox: bundle exec rails c -s
-create a course, a few grading periods, and a grading period group. Add the grading periods to the group. Assign
the grading period group to the course.
$ course = Course.create
$ grading_period1 = GradingPeriod.create(weight: 25.0, start_date: Time.zone.now, end_date: 2.days.from_now)
$ grading_period2 = GradingPeriod.create(weight: 30.0, start_date: Time.zone.now, end_date: 2.days.from_now)
$ grading_period_group = GradingPeriodGroup.create()
$ grading_period_group.grading_periods << grading_period1
$ grading_period_group.grading_periods << grading_period2
$ grading_period_group.course = course
-verify the associations are working as expected, i.e. a GradingPeriodGroup has GradingPeriods, a GradingPeriod
belongs to a GradingPeriodGroup, and a GradingPeriodGroup belongs to a course or account.
$ grading_period_group.grading_periods #should return an array containing grading_period1 and grading_period2
$ grading_period1.grading_period_group #should return grading_period_group
$ grading_period2.grading_period_group #should return grading_period_group
$ grading_period_group.course #should return course
$ grading_period_group.account #should return nil (should not throw error)
Change-Id: I9d7465431dabd2afa18e7a8a33706b9a78a94cd1
Reviewed-on: https://gerrit.instructure.com/43512
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Josh Simpson <jsimpson@instructure.com>
QA-Review: Amber Taniuchi <amber@instructure.com>
Reviewed-by: Jacob Fugal <jacob@instructure.com>
Product-Review: Spencer Olson <solson@instructure.com>
2014-10-30 02:42:41 +08:00
has_many :grading_period_groups , dependent : :destroy
2014-12-13 07:30:11 +08:00
has_many :grading_periods , through : :grading_period_groups
2014-11-14 02:35:24 +08:00
has_many :usage_rights , as : :context , class_name : 'UsageRights' , dependent : :destroy
2012-01-04 04:30:49 +08:00
2014-11-25 06:23:06 +08:00
has_many :sis_post_grades_statuses
2015-05-19 01:33:29 +08:00
has_many :progresses , as : :context
2015-05-13 23:06:20 +08:00
has_many :gradebook_csvs , inverse_of : :course
2014-11-25 06:23:06 +08:00
2016-02-17 05:51:39 +08:00
prepend Profile :: Association
2013-01-22 08:35:54 +08:00
2011-02-01 09:57:29 +08:00
before_save :assign_uuid
2013-08-08 06:19:48 +08:00
before_validation :assert_defaults
2011-02-01 09:57:29 +08:00
before_save :update_enrollments_later
allow displaing total grade as points for students
fixes CNVS-9874
when teacher choses to show total grade as points in GB2, a setting is saved
student grade summary page shows the total grade in same format
if assignment groups are weighted, grade is displayed as a percentage again
test plan:
- new addition:
- go to a course where GB2 is displaying totals as points BUT has no DB setting about show_point_totals
(ask mike if you need help getting to this state)
- as a student look at the grade summary page, total should be a percent
- as a teacher in that class, go to GB2 and wait for 5 seconds
- as that same student, go back to grade summary, total should be points now
- as a teacher, change total grade to show as points in GB2
- when you return to the GB, even from a new browser, this should be the same
- in GB, tooltip should show points if cell shows percent, and visa vera
- as a student in that class, you should see your total grade as points
- grade tooltip should show points if cell shows percent, and visa vera
- switch to weighted assignment groups
- total grade as a teacher and student should switch back to percent
- switch back to non-weighted
- total grade should stay a percent until explicitly changed back to points
- GB2 weighting and points/percent switching should have no unintended effects on the GB
- student grade summary page should display everything in a consistent manner
Change-Id: Id0c4f496576c226eb7000d6684a37faf0b439359
Reviewed-on: https://gerrit.instructure.com/28780
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Caleb Guanzon <cguanzon@instructure.com>
Reviewed-by: Simon Williams <simon@instructure.com>
Product-Review: Mike Nomitch <mnomitch@instructure.com>
2014-01-15 01:06:03 +08:00
before_save :update_show_total_grade_as_on_weighting_scheme_change
2011-04-13 01:03:21 +08:00
after_save :update_final_scores_on_weighting_scheme_change
2011-02-01 09:57:29 +08:00
after_save :update_account_associations_if_changed
2012-05-18 00:51:31 +08:00
after_save :set_self_enrollment_code
2015-02-21 03:45:22 +08:00
2015-01-14 06:58:53 +08:00
before_save :touch_root_folder_if_necessary
2015-06-16 23:37:16 +08:00
before_validation :verify_unique_ids
2015-12-01 22:59:53 +08:00
validate :validate_course_dates
2013-08-08 06:19:48 +08:00
validates_presence_of :account_id , :root_account_id , :enrollment_term_id , :workflow_state
2011-02-01 09:57:29 +08:00
validates_length_of :syllabus_body , :maximum = > maximum_long_text_length , :allow_nil = > true , :allow_blank = > true
2012-09-21 00:39:03 +08:00
validates_length_of :name , :maximum = > maximum_string_length , :allow_nil = > true , :allow_blank = > true
2013-12-08 15:43:19 +08:00
validates_length_of :sis_source_id , :maximum = > maximum_string_length , :allow_nil = > true , :allow_blank = > false
2012-09-21 00:39:03 +08:00
validates_length_of :course_code , :maximum = > maximum_string_length , :allow_nil = > true , :allow_blank = > true
2011-07-13 04:31:40 +08:00
validates_locale :allow_nil = > true
2012-01-04 04:30:49 +08:00
2014-01-29 05:29:09 +08:00
sanitize_field :syllabus_body , CanvasSanitize :: SANITIZE
2012-01-04 04:30:49 +08:00
2011-09-22 01:36:45 +08:00
include StickySisFields
2012-05-01 02:59:40 +08:00
are_sis_sticky :name , :course_code , :start_at , :conclude_at , :restrict_enrollments_to_course_dates , :enrollment_term_id , :workflow_state
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
2014-08-07 00:18:14 +08:00
include ContentNotices
define_content_notice :import_in_progress ,
icon_class : 'icon-import-content' ,
alert_class : 'alert-info import-in-progress-notice' ,
template : 'courses/import_in_progress_notice' ,
should_show : - > ( course , user ) do
course . grants_right? ( user , :manage_content )
end
2011-02-01 09:57:29 +08:00
has_a_broadcast_policy
2012-01-04 04:30:49 +08:00
2014-01-14 04:25:10 +08:00
def [] ( attr )
attr . to_s == 'asset_string' ? self . asset_string : super
end
2012-04-24 07:46:19 +08:00
def events_for ( user )
2012-06-28 01:36:17 +08:00
if user
CalendarEvent .
active .
for_user_and_context_codes ( user , [ asset_string ] ) .
2015-07-17 05:53:07 +08:00
preload ( :child_events ) .
2012-06-28 01:36:17 +08:00
reject ( & :hidden? ) +
AppointmentGroup . manageable_by ( user , [ asset_string ] ) +
2015-07-10 01:15:52 +08:00
user . assignments_visible_in_course ( self )
2012-06-28 01:36:17 +08:00
else
2015-07-17 05:53:07 +08:00
calendar_events . active . preload ( :child_events ) . reject ( & :hidden? ) +
2014-07-16 00:03:56 +08:00
assignments . active
2012-06-28 01:36:17 +08:00
end
2012-04-24 07:46:19 +08:00
end
2011-05-10 00:14:16 +08:00
def self . skip_updating_account_associations ( & block )
2015-03-18 03:21:11 +08:00
if @skip_updating_account_associations
2011-05-17 00:27:35 +08:00
block . call
else
begin
@skip_updating_account_associations = true
block . call
ensure
@skip_updating_account_associations = false
end
end
2011-05-10 00:14:16 +08:00
end
def self . skip_updating_account_associations?
! ! @skip_updating_account_associations
end
2011-02-01 09:57:29 +08:00
def update_account_associations_if_changed
2015-01-14 04:10:14 +08:00
if ( self . root_account_id_changed? || self . account_id_changed? ) && ! self . class . skip_updating_account_associations?
send_now_or_later_if_production ( new_record? ? :now : :later , :update_account_associations )
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 module_based?
Rails . cache . fetch ( [ 'module_based_course' , self ] . cache_key ) do
self . context_modules . active . any? { | m | m . completion_requirements && ! m . completion_requirements . empty? }
end
end
2012-01-04 04:30:49 +08:00
2013-02-06 05:59:01 +08:00
def modules_visible_to ( user )
2015-07-07 07:05:07 +08:00
if self . grants_right? ( user , :view_unpublished_items )
2013-02-06 05:59:01 +08:00
self . context_modules . not_deleted
else
self . context_modules . active
end
end
2013-08-22 06:36:47 +08:00
def module_items_visible_to ( user )
2015-07-07 07:05:07 +08:00
if user_is_teacher = self . grants_right? ( user , :view_unpublished_items )
2014-08-08 05:52:54 +08:00
tags = self . context_module_tags . not_deleted . joins ( :context_module ) . where ( " context_modules.workflow_state <> 'deleted' " )
2013-08-22 06:36:47 +08:00
else
2014-08-08 05:52:54 +08:00
tags = self . context_module_tags . active . joins ( :context_module ) . where ( :context_modules = > { :workflow_state = > 'active' } )
2013-08-22 06:36:47 +08:00
end
2014-08-08 05:52:54 +08:00
2014-09-12 23:56:25 +08:00
if self . feature_enabled? ( :differentiated_assignments )
2014-09-30 06:10:49 +08:00
tags = DifferentiableAssignment . scope_filter ( tags , user , self , is_teacher : user_is_teacher )
2014-08-08 05:52:54 +08:00
end
tags
2013-08-22 06:36:47 +08:00
end
2015-06-23 04:42:13 +08:00
def sequential_module_item_ids
Rails . cache . fetch ( [ 'ordered_module_item_ids' , self ] . cache_key ) do
self . context_module_tags . not_deleted . joins ( :context_module ) .
where ( " context_modules.workflow_state <> 'deleted' " ) .
where ( " content_tags.content_type <> 'ContextModuleSubHeader' " ) .
reorder ( " COALESCE(context_modules.position, 0), context_modules.id, content_tags.position NULLS LAST " ) .
pluck ( :id )
end
end
2015-06-16 23:37:16 +08:00
def verify_unique_ids
2012-06-18 23:15:30 +08:00
infer_root_account unless self . root_account_id
2015-01-14 01:50:19 +08:00
2015-06-16 23:37:16 +08:00
is_unique = true
if self . sis_source_id && ( root_account_id_changed? || sis_source_id_changed? )
scope = root_account . all_courses . where ( sis_source_id : self . sis_source_id )
scope = scope . where ( " id<>? " , self ) unless self . new_record?
if scope . exists?
is_unique = false
self . errors . add ( :sis_source_id , t ( 'errors.sis_in_use' , " SIS ID \" %{sis_id} \" is already in use " ,
:sis_id = > self . sis_source_id ) )
end
end
2015-01-14 01:50:19 +08:00
2015-06-16 23:37:16 +08:00
if self . integration_id && ( root_account_id_changed? || integration_id_changed? )
scope = root_account . all_courses . where ( integration_id : self . integration_id )
scope = scope . where ( " id<>? " , self ) unless self . new_record?
if scope . exists?
is_unique = false
self . errors . add ( :integration_id , t ( " Integration ID \" %{int_id} \" is already in use " ,
:int_id = > self . integration_id ) )
end
end
2012-01-04 04:30:49 +08:00
2015-06-16 23:37:16 +08:00
is_unique
2011-06-01 04:47:28 +08:00
end
2012-01-04 04:30:49 +08:00
2015-12-01 22:59:53 +08:00
def validate_course_dates
if start_at . present? && conclude_at . present? && conclude_at < start_at
self . errors . add ( :conclude_at , t ( " End date cannot be before start date " ) )
false
else
true
end
end
2011-02-01 09:57:29 +08:00
def public_license?
2014-11-14 02:35:24 +08:00
license && self . class . public_license? ( license )
2011-02-01 09:57:29 +08:00
end
2011-06-22 00:23:08 +08:00
def license_data
licenses = self . class . licenses
licenses [ license ] || licenses [ 'private' ]
end
2011-02-01 09:57:29 +08:00
def license_url
license_data [ :license_url ]
end
2011-06-22 00:23:08 +08:00
2011-02-01 09:57:29 +08:00
def readable_license
license_data [ :readable_license ]
end
2011-06-22 00:23:08 +08:00
2014-08-10 02:05:27 +08:00
def unpublishable?
ids = self . all_real_students . pluck :id
2015-01-06 00:36:48 +08:00
! self . submissions . with_assignment . with_point_data . where ( :user_id = > ids ) . exists?
2014-08-10 02:05:27 +08:00
end
2011-08-18 03:33:10 +08:00
def self . update_account_associations ( courses_or_course_ids , opts = { } )
return [ ] if courses_or_course_ids . empty?
opts . reverse_merge! :account_chain_cache = > { }
account_chain_cache = opts [ :account_chain_cache ]
# Split it up into manageable chunks
user_ids_to_update_account_associations = [ ]
if courses_or_course_ids . length > 500
opts = opts . dup
opts . reverse_merge! :skip_user_account_associations = > true
courses_or_course_ids . uniq . compact . each_slice ( 500 ) do | courses_or_course_ids_slice |
user_ids_to_update_account_associations += update_account_associations ( courses_or_course_ids_slice , opts )
end
else
if courses_or_course_ids . first . is_a? Course
courses = courses_or_course_ids
2015-12-19 05:47:46 +08:00
ActiveRecord :: Associations :: Preloader . new . preload ( courses , :course_sections = > :nonxlist_course )
2011-08-18 03:33:10 +08:00
course_ids = courses . map ( & :id )
else
course_ids = courses_or_course_ids
2013-06-04 04:52:42 +08:00
courses = Course . where ( :id = > course_ids ) .
2015-07-17 05:53:07 +08:00
preload ( :course_sections = > [ :course , :nonxlist_course ] ) .
2015-07-25 00:01:44 +08:00
select ( [ :id , :account_id ] ) . to_a
2011-08-18 03:33:10 +08:00
end
course_ids_to_update_user_account_associations = [ ]
CourseAccountAssociation . transaction do
current_associations = { }
to_delete = [ ]
2013-03-19 03:07:47 +08:00
CourseAccountAssociation . where ( :course_id = > course_ids ) . each do | aa |
2011-08-18 03:33:10 +08:00
key = [ aa . course_section_id , aa . account_id ]
current_course_associations = current_associations [ aa . course_id ] || = { }
2013-01-29 06:26:26 +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_course_associations . has_key? ( key )
to_delete << aa . id
next
end
current_course_associations [ key ] = [ aa . id , aa . depth ]
end
courses . each do | course |
did_an_update = false
current_course_associations = current_associations [ course . id ] || { }
# Courses are tied to accounts directly and through sections and crosslisted courses
( course . course_sections + [ nil ] ) . each do | section |
next if section && ! section . active?
section . course = course if section
2013-01-29 06:26:26 +08:00
starting_account_ids = [ course . account_id , section . try ( :course ) . try ( :account_id ) , section . try ( :nonxlist_course ) . try ( :account_id ) ] . compact . uniq
2011-08-18 03:33:10 +08:00
account_ids_with_depth = User . calculate_account_associations_from_accounts ( starting_account_ids , account_chain_cache ) . map
account_ids_with_depth . each do | account_id_with_depth |
account_id = account_id_with_depth [ 0 ]
depth = account_id_with_depth [ 1 ]
key = [ section . try ( :id ) , account_id ]
association = current_course_associations [ key ]
if association . nil?
# new association, create it
2014-05-08 05:11:33 +08:00
begin
2014-07-07 22:59:03 +08:00
course . transaction ( requires_new : true ) do
course . course_account_associations . create! do | aa |
aa . course_section_id = section . try ( :id )
aa . account_id = account_id
aa . depth = depth
end
2014-05-08 05:11:33 +08:00
end
2014-09-12 04:01:11 +08:00
rescue ActiveRecord :: RecordNotUnique
2014-05-08 05:11:33 +08:00
course . course_account_associations . where ( course_section_id : section ,
account_id : account_id ) . update_all ( :depth = > depth )
2011-08-18 03:33:10 +08:00
end
did_an_update = true
else
if association [ 1 ] != depth
2013-03-19 03:07:47 +08:00
CourseAccountAssociation . where ( :id = > association [ 0 ] ) . update_all ( :depth = > depth )
2011-08-18 03:33:10 +08:00
did_an_update = true
end
# remove from list of existing
current_course_associations . delete ( key )
end
end
end
did_an_update || = ! current_course_associations . empty?
if did_an_update
course . course_account_associations . reset
course . non_unique_associated_accounts . reset
course_ids_to_update_user_account_associations << course . id
end
end
to_delete += current_associations . map { | k , v | v . map { | k2 , v2 | v2 [ 0 ] } } . flatten
unless to_delete . empty?
2013-03-19 03:07:47 +08:00
CourseAccountAssociation . where ( :id = > to_delete ) . delete_all
2011-08-18 03:33:10 +08:00
end
end
2013-03-19 03:07:47 +08:00
user_ids_to_update_account_associations = Enrollment .
where ( " course_id IN (?) AND workflow_state<>'deleted' " , course_ids_to_update_user_account_associations ) .
group ( :user_id ) . pluck ( :user_id ) unless course_ids_to_update_user_account_associations . empty?
2011-02-01 09:57:29 +08:00
end
2011-08-18 03:33:10 +08:00
User . update_account_associations ( user_ids_to_update_account_associations , :account_chain_cache = > account_chain_cache ) unless user_ids_to_update_account_associations . empty? || opts [ :skip_user_account_associations ]
user_ids_to_update_account_associations
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2011-08-18 03:33:10 +08:00
def update_account_associations
2013-01-26 01:32:08 +08:00
self . shard . activate do
Course . update_account_associations ( [ self ] )
end
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2011-05-17 00:27:35 +08:00
def associated_accounts
2015-07-25 00:01:44 +08:00
accounts = self . non_unique_associated_accounts . to_a . uniq
2014-05-08 03:39:29 +08:00
accounts << self . account if account_id && ! accounts . find { | a | a . id == account_id }
accounts << self . root_account if root_account_id && ! accounts . find { | a | a . id == root_account_id }
accounts
2011-05-17 00:27:35 +08:00
end
2012-01-04 04:30:49 +08:00
2014-07-02 03:38:26 +08:00
scope :recently_started , - > { where ( :start_at = > 1 . month . ago .. Time . zone . now ) . order ( " start_at DESC " ) . limit ( 10 ) }
scope :recently_ended , - > { where ( :conclude_at = > 1 . month . ago .. Time . zone . now ) . order ( " start_at DESC " ) . limit ( 10 ) }
2015-07-16 03:39:46 +08:00
scope :recently_created , - > { where ( " created_at>? " , 1 . month . ago ) . order ( " created_at DESC " ) . limit ( 50 ) . preload ( :teachers ) }
2015-12-22 05:03:24 +08:00
scope :for_term , lambda { | term | term ? where ( :enrollment_term_id = > term ) : all }
2014-07-02 03:38:26 +08:00
scope :active_first , - > { order ( " CASE WHEN courses.workflow_state='available' THEN 0 ELSE 1 END, #{ best_unicode_collation_key ( 'name' ) } " ) }
2014-09-04 04:39:02 +08:00
scope :name_like , lambda { | name | where ( coalesced_wildcard ( 'courses.name' , 'courses.sis_source_id' , 'courses.course_code' , name ) ) }
2013-03-21 03:38:19 +08:00
scope :needs_account , lambda { | account , limit | where ( :account_id = > nil , :root_account_id = > account ) . limit ( limit ) }
2014-07-02 03:38:26 +08:00
scope :active , - > { where ( " courses.workflow_state<>'deleted' " ) }
2013-03-21 03:38:19 +08:00
scope :least_recently_updated , lambda { | limit | order ( :updated_at ) . limit ( limit ) }
scope :manageable_by_user , lambda { | * args |
2012-02-21 08:23:23 +08:00
# args[0] should be user_id, args[1], if true, will include completed
# enrollments as well as active enrollments
user_id = args [ 0 ]
workflow_states = ( args [ 1 ] . present? ? %w{ 'active' 'completed' } : %w{ 'active' } ) . join ( ', ' )
2013-03-21 03:38:19 +08:00
uniq . joins ( " INNER JOIN (
2015-07-16 05:14:09 +08:00
SELECT caa . course_id , au . user_id FROM #{CourseAccountAssociation.quoted_table_name} AS caa
2015-10-27 04:25:42 +08:00
INNER JOIN #{Account.quoted_table_name} AS a ON a.id = caa.account_id AND a.workflow_state = 'active'
INNER JOIN #{AccountUser.quoted_table_name} AS au ON au.account_id = a.id AND au.user_id = #{user_id.to_i}
2015-07-16 05:14:09 +08:00
UNION SELECT courses . id AS course_id , e . user_id FROM #{Course.quoted_table_name}
INNER JOIN #{Enrollment.quoted_table_name} AS e ON e.course_id = courses.id AND e.user_id = #{user_id.to_i}
2012-02-21 08:23:23 +08:00
AND e . workflow_state IN ( #{workflow_states}) AND e.type IN ('TeacherEnrollment', 'TaEnrollment', 'DesignerEnrollment')
2011-11-18 03:25:22 +08:00
WHERE courses . workflow_state < > 'deleted' ) as course_users
2013-03-21 03:38:19 +08:00
ON course_users . course_id = courses . id " )
2011-02-01 09:57:29 +08:00
}
2014-07-02 03:38:26 +08:00
scope :not_deleted , - > { where ( " workflow_state<>'deleted' " ) }
2011-02-01 09:57:29 +08:00
2014-07-02 03:38:26 +08:00
scope :with_enrollments , - > {
2013-10-29 00:17:08 +08:00
where ( " EXISTS (?) " , Enrollment . active . where ( " enrollments.course_id=courses.id " ) )
2011-08-17 05:49:53 +08:00
}
2015-10-16 13:03:08 +08:00
scope :with_enrollment_types , - > ( types ) {
types = types . map { | type | " #{ type . capitalize } Enrollment " }
where ( " EXISTS (?) " , Enrollment . active . where ( " enrollments.course_id=courses.id " ) . where ( type : types ) )
}
2014-07-02 03:38:26 +08:00
scope :without_enrollments , - > {
2013-10-29 00:17:08 +08:00
where ( " NOT EXISTS (?) " , Enrollment . active . where ( " enrollments.course_id=courses.id " ) )
2012-11-01 06:46:10 +08:00
}
2014-07-02 03:38:26 +08:00
scope :completed , - > {
2013-03-21 03:38:19 +08:00
joins ( :enrollment_term ) .
where ( " courses.workflow_state='completed' OR courses.conclude_at<? OR enrollment_terms.end_at<? " , Time . now . utc , Time . now . utc )
2012-11-01 06:46:10 +08:00
}
2014-07-02 03:38:26 +08:00
scope :not_completed , - > {
2013-03-21 03:38:19 +08:00
joins ( :enrollment_term ) .
where ( " courses.workflow_state<>'completed' AND
( courses . conclude_at IS NULL OR courses . conclude_at > = ?) AND
( enrollment_terms . end_at IS NULL OR enrollment_terms . end_at > = ?) " , Time.now.utc, Time.now.utc)
2012-11-01 06:46:10 +08:00
}
2013-03-21 03:38:19 +08:00
scope :by_teachers , lambda { | teacher_ids |
teacher_ids . empty? ?
2013-10-26 05:53:23 +08:00
none :
2013-10-29 00:17:08 +08:00
where ( " EXISTS (?) " , Enrollment . active . where ( " enrollments.course_id=courses.id AND enrollments.type='TeacherEnrollment' AND enrollments.user_id IN (?) " , teacher_ids ) )
2012-11-01 06:46:10 +08:00
}
2014-07-02 03:38:26 +08:00
scope :by_associated_accounts , lambda { | account_ids |
2013-03-21 03:38:19 +08:00
account_ids . empty? ?
2013-10-26 05:53:23 +08:00
none :
2013-10-29 00:17:08 +08:00
where ( " EXISTS (?) " , CourseAccountAssociation . where ( " course_account_associations.course_id=courses.id AND course_account_associations.account_id IN (?) " , account_ids ) )
2012-11-01 06:46:10 +08:00
}
2015-06-17 09:17:28 +08:00
scope :published , - > { where ( workflow_state : %w( available completed ) ) }
scope :unpublished , - > { where ( workflow_state : %w( created claimed ) ) }
2011-08-17 05:49:53 +08:00
2014-07-02 03:38:26 +08:00
scope :deleted , - > { where ( :workflow_state = > 'deleted' ) }
2013-03-05 01:20:41 +08:00
2011-02-01 09:57:29 +08:00
set_broadcast_policy do | p |
p . dispatch :grade_weight_changed
p . to { participating_students }
2012-01-04 04:30:49 +08:00
p . whenever { | record |
( record . available? && @grade_weight_changed ) ||
2015-05-19 06:13:49 +08:00
(
record . prior_version . present? &&
record . changed_in_state ( :available , :fields = > :group_weighting_scheme )
)
2011-02-01 09:57:29 +08:00
}
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
p . dispatch :new_course
p . to { self . root_account . account_users }
p . whenever { | record |
record . root_account &&
2011-06-22 00:23:08 +08:00
( ( record . just_created && record . name != Course . default_name ) ||
2015-06-11 07:33:43 +08:00
( record . prior_version . present? &&
record . prior_version . name == Course . default_name &&
record . name != Course . default_name )
)
2011-02-01 09:57:29 +08:00
}
end
2011-06-22 00:23:08 +08:00
def self . default_name
2011-07-01 04:36:59 +08:00
# TODO i18n
2011-06-22 00:23:08 +08:00
t ( 'default_name' , " My Course " )
end
2012-01-04 04:30:49 +08:00
2013-11-21 08:15:12 +08:00
def users_not_in_groups ( groups , opts = { } )
scope = User . joins ( :not_ended_enrollments ) .
where ( enrollments : { course_id : self , type : 'StudentEnrollment' } ) .
where ( Group . not_in_group_sql_fragment ( groups . map ( & :id ) ) ) .
2015-07-11 00:48:45 +08:00
select ( " users.id, users.name, users.updated_at " ) . uniq
2013-11-21 08:15:12 +08:00
scope = scope . select ( opts [ :order ] ) . order ( opts [ :order ] ) if opts [ :order ]
scope
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2012-01-13 07:57:58 +08:00
def instructors_in_charge_of ( user_id )
2013-11-15 05:17:35 +08:00
scope = current_enrollments .
where ( :course_id = > self , :user_id = > user_id ) .
where ( " course_section_id IS NOT NULL " )
2014-07-24 01:14:22 +08:00
section_ids = scope . uniq . pluck ( :course_section_id )
2012-02-18 10:49:16 +08:00
participating_instructors . restrict_to_sections ( section_ids )
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2012-10-20 04:04:42 +08:00
def user_is_instructor? ( user )
2011-02-01 09:57:29 +08:00
return unless user
2015-08-12 22:11:58 +08:00
RequestCache . cache ( 'user_is_instructor' , self , user ) do
2015-06-26 03:35:23 +08:00
Rails . cache . fetch ( [ self , user , " course_user_is_instructor " ] . cache_key ) do
2015-10-29 22:14:10 +08:00
user . cached_current_enrollments ( preload_dates : true ) . any? { | e | e . course_id == self . id && e . participating_instructor? }
2015-06-26 03:35:23 +08:00
end
2011-02-01 09:57:29 +08:00
end
end
2012-01-04 04:30:49 +08:00
2012-12-07 14:28:37 +08:00
def user_is_student? ( user , opts = { } )
2011-02-01 09:57:29 +08:00
return unless user
2015-08-12 22:11:58 +08:00
RequestCache . cache ( 'user_is_student' , self , user , opts ) do
2015-06-26 03:35:23 +08:00
Rails . cache . fetch ( [ self , user , " course_user_is_student " , opts [ :include_future ] ] . cache_key ) do
add scope_assignments_to_student parameter
scope_assignments_to_student can now be passed
as an argument to assignment_groups_controller#
index. if this argument is passed in as true,
assignments will only be returned if they apply
to the requesting user in the given grading period.
in addition, when requesting multiple grading
periods data for these endpoints:
GET /api/v1/courses
GET /api/v1/courses/:course_id
GET /api/v1/users/:user_id/courses
GET /api/v1/users/self/favorites/courses
we now include the current_grading_period_id in the response.
closes CNVS-26847
test plan 1: test scope_assignments_to_student
- create a course, and enroll two students (
i will refer to these as Student A and
Student B).
- enable Multiple Grading Periods for the
course.
- create a grading period that starts Feb 1
and ends Feb 29.
- create a grading period that starts March 1
and ends March 31.
- create an assignment. assign it to Student A
on March 15. Assign it to Student B on Feb 15.
i'll refer to this as Assignment 1.
- create another assignment. assign it to Student
A on April 15. Assign it to Student B on March 15.
i'll refer to this as Assigment 2.
- as Student A, make a GET request to
/api/v1/courses/:course_id/assignment_groups
(replace :course_id with the id of the course
created in the first step). include the following
params with the request:
format: 'json'
include[]: 'assignments'
include[]: 'assignment_visibility'
include[]: 'overrides'
grading_period_id: 5 #don't actually put 5 here,
put the id of the March grading period
- verify you get two assignments back (they will
be nested in the response, under a key named
"assignments"). the assignment IDs should match
the ids of Assignment 1 and Assignment 2.
- now make another GET request with Student A with
the same params, but this time also add the
following param:
scope_assignments_to_student: true
- verify you get only one assignment back. the
assignment id should match the id of Assignment
1.
test plan 2: verify current_grading_period_id in responses
- hit the api/v1/courses, api/v1/courses/:id,
api/v1/users/:user_id/courses, and
api/v1/users/self/favorites/courses endpoints
and pass in 'current_grading_period_scores'
as an include[] argument. verify you get a
`current_grading_period_id` in the response.
- ping me with any questions :D
Change-Id: I5f906a8cddbc63002cadf533c46c69de0d29830f
Reviewed-on: https://gerrit.instructure.com/71169
Tested-by: Jenkins
Reviewed-by: Keith T. Garner <kgarner@instructure.com>
QA-Review: KC Naegle <knaegle@instructure.com>
Product-Review: Spencer Olson <solson@instructure.com>
2016-01-29 03:18:18 +08:00
current_enrollments = user . cached_current_enrollments (
preload_dates : true , include_future : opts [ :include_future ] )
current_enrollments . any? do | enrollment |
enrollment . course_id == self . id &&
enrollment . student_with_conditions? (
include_future : opts [ :include_future ] ,
include_fake_student : opts [ :include_fake_student ]
)
end
2015-06-26 03:35:23 +08:00
end
2011-02-01 09:57:29 +08:00
end
end
2012-01-04 04:30:49 +08:00
2012-10-20 04:04:42 +08:00
def user_has_been_instructor? ( user )
2012-03-02 04:47:45 +08:00
return unless user
2014-01-08 02:08:17 +08:00
# enrollments should be on the course's shard
2015-08-12 22:11:58 +08:00
RequestCache . cache ( 'user_has_been_instructor' , self , user ) do
2015-06-26 03:35:23 +08:00
self . shard . activate do
Rails . cache . fetch ( [ self , user , " course_user_has_been_instructor " ] . cache_key ) do
# active here is !deleted; it still includes concluded, etc.
self . instructor_enrollments . active . where ( user_id : user ) . exists?
end
2014-01-08 02:08:17 +08:00
end
2012-03-02 04:47:45 +08:00
end
end
2012-10-30 01:00:07 +08:00
def user_has_been_admin? ( user )
return unless user
2015-08-12 22:11:58 +08:00
RequestCache . cache ( 'user_has_been_admin' , self , user ) do
2015-06-26 03:35:23 +08:00
Rails . cache . fetch ( [ self , user , " course_user_has_been_admin " ] . cache_key ) do
# active here is !deleted; it still includes concluded, etc.
self . admin_enrollments . active . where ( user_id : user ) . exists?
end
2012-10-30 01:00:07 +08:00
end
end
def user_has_been_observer? ( user )
return unless user
2015-08-12 22:11:58 +08:00
RequestCache . cache ( 'user_has_been_observer' , self , user ) do
2015-06-26 03:35:23 +08:00
Rails . cache . fetch ( [ self , user , " course_user_has_been_observer " ] . cache_key ) do
# active here is !deleted; it still includes concluded, etc.
2015-09-04 07:17:24 +08:00
self . observer_enrollments . shard ( self ) . active . where ( user_id : user ) . exists?
2015-06-26 03:35:23 +08:00
end
2012-10-30 01:00:07 +08:00
end
end
2012-03-02 04:47:45 +08:00
def user_has_been_student? ( user )
return unless user
2015-08-12 22:11:58 +08:00
RequestCache . cache ( 'user_has_been_student' , self , user ) do
2015-06-26 03:35:23 +08:00
Rails . cache . fetch ( [ self , user , " course_user_has_been_student " ] . cache_key ) do
self . all_student_enrollments . where ( user_id : user ) . exists?
end
2012-03-02 04:47:45 +08:00
end
end
2012-10-30 01:00:07 +08:00
def user_has_no_enrollments? ( user )
return unless user
2015-08-12 22:11:58 +08:00
RequestCache . cache ( 'user_has_no_enrollments' , self , user ) do
2015-06-26 03:35:23 +08:00
Rails . cache . fetch ( [ self , user , " course_user_has_no_enrollments " ] . cache_key ) do
! enrollments . where ( user_id : user ) . exists?
end
2012-10-30 01:00:07 +08:00
end
end
2012-07-27 04:32:17 +08:00
# Public: Determine if a group weighting scheme should be applied.
#
# Returns boolean.
def apply_group_weights?
group_weighting_scheme == 'percent'
end
2013-06-27 23:33:28 +08:00
def apply_assignment_group_weights = ( apply )
if apply
self . group_weighting_scheme = 'percent'
else
self . group_weighting_scheme = 'equal'
end
end
2011-02-01 09:57:29 +08:00
def grade_weight_changed!
@grade_weight_changed = true
self . save!
@grade_weight_changed = false
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def membership_for_user ( user )
2014-09-12 03:44:34 +08:00
self . enrollments . where ( user_id : user ) . first if user
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2012-06-18 23:15:30 +08:00
def infer_root_account
# This is a bit tricky. Basically, it ensures a is the current account;
# if account is not loaded, it will not double load (because it's
# already cached). If it's already loaded, and correct, it again will
# only use the cache. If it's already loaded and the wrong one, it will
# force reload
a = self . account ( self . account && self . account . id != self . account_id )
2015-08-26 07:52:41 +08:00
self . root_account = a if a . root_account?
2012-06-18 23:15:30 +08:00
self . root_account_id = a . root_account_id if a
self . root_account_id || = a . id if a
# Ditto
self . root_account ( self . root_account && self . root_account . id != self . root_account_id )
end
2011-02-01 09:57:29 +08:00
def assert_defaults
self . tab_configuration || = [ ] unless self . tab_configuration == [ ]
self . name = nil if self . name && self . name . strip . empty?
2011-06-22 00:23:08 +08:00
self . name || = t ( 'missing_name' , " Unnamed Course " )
2014-10-21 05:38:15 +08:00
self . course_code = nil if self . course_code == ''
2011-02-01 09:57:29 +08:00
if ! self . course_code && self . name
res = [ ]
split = self . name . split ( / \ s / )
res << split [ 0 ]
res << split [ 1 .. - 1 ] . find { | txt | txt . match ( / \ d / ) } rescue nil
self . course_code = res . compact . join ( " " )
end
@group_weighting_scheme_changed = self . group_weighting_scheme_changed?
if self . account_id && self . account_id_changed?
2012-06-18 23:15:30 +08:00
infer_root_account
2011-02-01 09:57:29 +08:00
end
if self . root_account_id && self . root_account_id_changed?
2011-05-07 00:42:55 +08:00
a = self . account ( self . account && self . account . id != self . account_id )
2011-02-01 09:57:29 +08:00
self . account_id = nil if self . account_id && self . account_id != self . root_account_id && a && a . root_account_id != self . root_account_id
self . account_id || = self . root_account_id
end
self . root_account_id || = Account . default . id
self . account_id || = self . root_account_id
2011-08-20 06:09:57 +08:00
self . enrollment_term = nil if self . enrollment_term . try ( :root_account_id ) != self . root_account_id
2011-02-01 09:57:29 +08:00
self . enrollment_term || = self . root_account . default_enrollment_term
self . allow_student_wiki_edits = ( self . default_wiki_editing_roles || " " ) . split ( ',' ) . include? ( 'students' )
2016-02-04 03:28:11 +08:00
if self . course_format && ! [ 'on_campus' , 'online' , 'blended' ] . include? ( self . course_format )
self . course_format = nil
end
2011-02-01 09:57:29 +08:00
true
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def update_course_section_names
return if @course_name_was == self . name || ! @course_name_was
sections = self . course_sections
2013-12-22 03:33:48 +08:00
fields_to_possibly_rename = [ :name ]
2011-02-01 09:57:29 +08:00
sections . each do | section |
something_changed = false
fields_to_possibly_rename . each do | field |
2012-01-04 04:30:49 +08:00
section . send ( " #{ field } = " , section . default_section ?
2011-02-01 09:57:29 +08:00
self . name :
( section . send ( field ) || self . name ) . sub ( @course_name_was , self . name ) )
something_changed = true if section . send ( field ) != section . send ( " #{ field } _was " )
end
if something_changed
2011-08-31 06:13:41 +08:00
attr_hash = { :updated_at = > Time . now . utc }
2011-02-01 09:57:29 +08:00
fields_to_possibly_rename . each { | key | attr_hash [ key ] = section . send ( key ) }
2013-03-19 03:07:47 +08:00
CourseSection . where ( :id = > section ) . update_all ( attr_hash )
2011-02-01 09:57:29 +08:00
end
end
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def update_enrollments_later
2011-08-20 06:09:57 +08:00
self . update_enrolled_users if ! self . new_record? && ! ( self . changes . keys & [ 'workflow_state' , 'name' , 'course_code' , 'start_at' , 'conclude_at' , 'enrollment_term_id' ] ) . empty?
2011-02-01 09:57:29 +08:00
true
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def update_enrolled_users
2015-10-14 22:24:22 +08:00
self . shard . activate do
if self . workflow_state_changed?
if self . completed?
2015-12-15 03:22:40 +08:00
Enrollment . where ( :course_id = > self , :workflow_state = > [ 'active' , 'invited' ] ) . update_all ( :workflow_state = > 'completed' , :completed_at = > Time . now . utc )
2015-10-14 22:24:22 +08:00
appointment_participants . active . current . update_all ( :workflow_state = > 'deleted' )
appointment_groups . each ( & :clear_cached_available_slots! )
elsif self . deleted?
enroll_scope = Enrollment . where ( " course_id=? AND workflow_state<>'deleted' " , self )
user_ids = enroll_scope . group ( :user_id ) . pluck ( :user_id ) . uniq
if user_ids . any?
enroll_scope . update_all ( :workflow_state = > 'deleted' )
User . send_later_if_production ( :update_account_associations , user_ids )
end
end
end
2011-11-22 01:55:44 +08:00
2015-10-14 22:24:22 +08:00
if self . root_account_id_changed?
CourseSection . where ( :course_id = > self ) . update_all ( :root_account_id = > self . root_account_id )
Enrollment . where ( :course_id = > self ) . update_all ( :root_account_id = > self . root_account_id )
end
2011-11-22 01:55:44 +08:00
2015-10-14 22:24:22 +08:00
Enrollment . where ( :course_id = > self ) . update_all ( :updated_at = > Time . now . utc )
2015-10-23 03:41:49 +08:00
User . where ( id : Enrollment . where ( course_id : self ) . select ( :user_id ) ) .
2015-10-14 22:24:22 +08:00
update_all ( updated_at : Time . now . utc )
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 self_enrollment_allowed?
2011-03-27 11:38:54 +08:00
! ! ( self . account && self . account . self_enrollment_allowed? ( self ) )
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2014-08-21 21:07:27 +08:00
def self_enrollment_enabled?
self . self_enrollment? && self . self_enrollment_allowed?
end
2011-02-01 09:57:29 +08:00
def self_enrollment_code
2013-01-29 08:36:31 +08:00
read_attribute ( :self_enrollment_code ) || set_self_enrollment_code
2012-05-18 00:51:31 +08:00
end
def set_self_enrollment_code
2014-08-21 21:07:27 +08:00
return if ! self_enrollment_enabled? || read_attribute ( :self_enrollment_code )
2012-05-18 00:51:31 +08:00
# subset of letters and numbers that are unambiguous
alphanums = 'ABCDEFGHJKLMNPRTWXY346789'
code_length = 6
# we're returning a 6-digit base-25(ish) code. that means there are ~250
# million possible codes. we should expect to see our first collision
# within the first 16k or so (thus the retry loop), but we won't risk ever
# exhausting a retry loop until we've used up about 15% or so of the
# keyspace. if needed, we can grow it at that point (but it's scoped to a
# shard, and not all courses will have enrollment codes, so that may not be
# necessary)
code = nil
self . class . unique_constraint_retry ( 10 ) do
code = code_length . times . map {
alphanums [ ( rand * alphanums . size ) . to_i , 1 ]
} . join
update_attribute :self_enrollment_code , code
end
code
end
2012-12-12 13:22:25 +08:00
def self_enrollment_limit_met?
2012-12-21 14:16:51 +08:00
self_enrollment_limit && self_enrolled_students . size > = self_enrollment_limit
2012-12-12 13:22:25 +08:00
end
2012-05-18 00:51:31 +08:00
def long_self_enrollment_code
2013-12-04 07:00:14 +08:00
@long_self_enrollment_code || = Digest :: MD5 . hexdigest ( " #{ uuid } _for_ #{ id } " )
2011-02-01 09:57:29 +08:00
end
2012-05-18 00:51:31 +08:00
# still include the old longer format, since links may be out there
def self_enrollment_codes
[ self_enrollment_code , long_self_enrollment_code ]
end
2012-01-04 04:30:49 +08:00
allow displaing total grade as points for students
fixes CNVS-9874
when teacher choses to show total grade as points in GB2, a setting is saved
student grade summary page shows the total grade in same format
if assignment groups are weighted, grade is displayed as a percentage again
test plan:
- new addition:
- go to a course where GB2 is displaying totals as points BUT has no DB setting about show_point_totals
(ask mike if you need help getting to this state)
- as a student look at the grade summary page, total should be a percent
- as a teacher in that class, go to GB2 and wait for 5 seconds
- as that same student, go back to grade summary, total should be points now
- as a teacher, change total grade to show as points in GB2
- when you return to the GB, even from a new browser, this should be the same
- in GB, tooltip should show points if cell shows percent, and visa vera
- as a student in that class, you should see your total grade as points
- grade tooltip should show points if cell shows percent, and visa vera
- switch to weighted assignment groups
- total grade as a teacher and student should switch back to percent
- switch back to non-weighted
- total grade should stay a percent until explicitly changed back to points
- GB2 weighting and points/percent switching should have no unintended effects on the GB
- student grade summary page should display everything in a consistent manner
Change-Id: Id0c4f496576c226eb7000d6684a37faf0b439359
Reviewed-on: https://gerrit.instructure.com/28780
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Caleb Guanzon <cguanzon@instructure.com>
Reviewed-by: Simon Williams <simon@instructure.com>
Product-Review: Mike Nomitch <mnomitch@instructure.com>
2014-01-15 01:06:03 +08:00
def update_show_total_grade_as_on_weighting_scheme_change
if group_weighting_scheme_changed? and self . group_weighting_scheme == 'percent'
self . show_total_grade_as_points = false
end
true
end
2015-01-14 06:58:53 +08:00
# to ensure permissions on the root folder are updated after hiding or showing the files tab
def touch_root_folder_if_necessary
if tab_configuration_changed?
2015-07-30 00:17:37 +08:00
files_tab_was_hidden = tab_configuration_was && tab_configuration_was . any? { | h | ! h . blank? && h [ 'id' ] == TAB_FILES && h [ 'hidden' ] }
2015-01-14 06:58:53 +08:00
Folder . root_folders ( self ) . each { | f | f . touch } if files_tab_was_hidden != tab_hidden? ( TAB_FILES )
end
true
end
2011-04-13 01:03:21 +08:00
def update_final_scores_on_weighting_scheme_change
2011-02-01 09:57:29 +08:00
if @group_weighting_scheme_changed
2015-12-29 03:23:33 +08:00
self . class . connection . after_transaction_commit { self . recompute_student_scores }
2011-02-01 09:57:29 +08:00
end
end
2012-01-04 04:30:49 +08:00
2013-10-04 03:47:36 +08:00
def recompute_student_scores ( student_ids = nil )
2015-02-13 11:08:45 +08:00
student_ids || = self . student_ids
Rails . logger . info " GRADES: recomputing scores in course= #{ global_id } students= #{ student_ids . inspect } "
Enrollment . recompute_final_score ( student_ids , self . id )
2011-02-01 09:57:29 +08:00
end
2012-04-05 04:12:28 +08:00
handle_asynchronously_if_production :recompute_student_scores ,
:singleton = > proc { | c | " recompute_student_scores: #{ c . global_id } " }
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def home_page
2013-05-17 05:02:04 +08:00
self . wiki . front_page
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 context_code
2013-03-08 08:08:47 +08:00
raise " DONT USE THIS, use .short_name instead " unless Rails . env . production?
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 allow_media_comments?
true || [ ] . include? ( self . id )
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def short_name
course_code
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def short_name = ( val )
write_attribute ( :course_code , val )
end
2012-01-04 04:30:49 +08:00
2012-12-27 08:51:39 +08:00
def short_name_slug
2014-04-11 01:37:36 +08:00
CanvasTextHelper . truncate_text ( short_name , :ellipsis = > '' )
2012-12-27 08:51:39 +08:00
end
2011-02-01 09:57:29 +08:00
# Allows the account to be set directly
belongs_to :account
2012-01-04 04:30:49 +08:00
2012-07-24 05:29:18 +08:00
def wiki_with_create
Wiki . wiki_for_context ( self )
2011-02-01 09:57:29 +08:00
end
2012-07-24 05:29:18 +08:00
alias_method_chain :wiki , :create
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
# A universal lookup for all messages.
def messages
Message . for ( self )
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def do_complete
self . conclude_at || = Time . now
end
2011-05-24 07:24:31 +08:00
def do_unconclude
self . conclude_at = nil
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def do_offer
self . start_at || = Time . now
send_later_if_production ( :invite_uninvited_students )
end
2012-01-04 04:30:49 +08:00
2014-06-07 03:28:41 +08:00
def do_claim
self . workflow_state = 'claimed'
end
2011-02-01 09:57:29 +08:00
def invite_uninvited_students
2014-09-12 03:44:34 +08:00
self . enrollments . where ( workflow_state : " creation_pending " ) . each do | e |
2011-02-01 09:57:29 +08:00
e . invite!
end
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
workflow do
state :created do
event :claim , :transitions_to = > :claimed
event :offer , :transitions_to = > :available
event :complete , :transitions_to = > :completed
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
state :claimed do
event :offer , :transitions_to = > :available
event :complete , :transitions_to = > :completed
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
state :available do
event :complete , :transitions_to = > :completed
2014-06-07 03:28:41 +08:00
event :claim , :transitions_to = > :claimed
2011-02-01 09:57:29 +08:00
end
2011-05-24 07:24:31 +08:00
state :completed do
event :unconclude , :transitions_to = > :available
end
2011-02-01 09:57:29 +08:00
state :deleted
end
2012-01-04 04:30:49 +08:00
2013-04-16 00:36:53 +08:00
def api_state
return 'unpublished' if workflow_state == 'created' || workflow_state == 'claimed'
workflow_state
end
2014-02-20 05:07:34 +08:00
2016-01-06 02:31:25 +08:00
alias_method :destroy_permanently! , :destroy
2011-02-01 09:57:29 +08:00
def destroy
self . workflow_state = 'deleted'
save!
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def call_event ( event )
self . send ( event ) if self . current_state . events . include? event . to_sym
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def claim_with_teacher ( user )
raise " Must provide a valid teacher " unless user
return unless self . state == :created
e = enroll_user ( user , 'TeacherEnrollment' , :enrollment_state = > 'active' ) #teacher(user)
claim
e
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def self . require_assignment_groups ( contexts )
courses = contexts . select { | c | c . is_a? ( Course ) }
2013-04-02 02:55:32 +08:00
groups = Shard . partition_by_shard ( courses ) do | shard_courses |
AssignmentGroup . select ( " id, context_id, context_type " ) . where ( :context_type = > " Course " , :context_id = > shard_courses )
end . index_by ( & :context_id )
courses . each do | course |
if ! groups [ course . id ]
course . require_assignment_group rescue nil
end
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 require_assignment_group
2013-04-02 02:55:32 +08:00
shard . activate do
2013-10-30 05:35:19 +08:00
return if Rails . cache . read ( [ 'has_assignment_group' , self ] . cache_key )
2013-04-02 02:55:32 +08:00
if self . assignment_groups . active . empty?
self . assignment_groups . create ( :name = > t ( '#assignment_group.default_name' , " Assignments " ) )
end
Rails . cache . write ( [ 'has_assignment_group' , self ] . cache_key , true )
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 self . create_unique ( uuid = nil , account_id = nil , root_account_id = nil )
2014-07-11 01:22:01 +08:00
uuid || = CanvasSlug . generate_securish_uuid
2014-09-12 03:44:34 +08:00
course = where ( uuid : uuid ) . first_or_initialize
2011-02-01 09:57:29 +08:00
course = Course . new if course . deleted?
2011-06-22 00:23:08 +08:00
course . name = self . default_name if course . new_record?
course . short_name = t ( 'default_short_name' , " Course-101 " ) if course . new_record?
2011-02-01 09:57:29 +08:00
course . account_id = account_id || root_account_id
course . root_account_id = root_account_id
course . save!
course
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def <=> ( other )
self . id < = > other . id
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def quota
Rails . cache . fetch ( [ 'default_quota' , self ] . cache_key ) do
2011-07-12 05:36:55 +08:00
storage_quota
2011-02-01 09:57:29 +08:00
end
end
2012-01-04 04:30:49 +08:00
2011-06-18 00:46:48 +08:00
def storage_quota_mb
2011-06-18 04:19:46 +08:00
storage_quota / 1 . megabyte
2011-06-18 00:46:48 +08:00
end
2012-01-04 04:30:49 +08:00
2011-06-18 00:46:48 +08:00
def storage_quota_mb = ( val )
2011-07-09 21:22:33 +08:00
self . storage_quota = val . try ( :to_i ) . try ( :megabytes )
2011-06-18 00:46:48 +08:00
end
2012-01-04 04:30:49 +08:00
2014-12-24 00:48:52 +08:00
def storage_quota_used_mb
Attachment . get_quota ( self ) [ :quota_used ] . to_f / 1 . megabyte
end
2011-07-12 05:36:55 +08:00
def storage_quota
return read_attribute ( :storage_quota ) ||
( self . account . default_storage_quota rescue nil ) ||
2013-10-05 04:02:49 +08:00
Setting . get ( 'course_default_quota' , 500 . megabytes . to_s ) . to_i
2011-07-12 05:36:55 +08:00
end
2011-02-01 09:57:29 +08:00
def storage_quota = ( val )
val = val . to_f
val = nil if val < = 0
if account && account . default_storage_quota == val
val = nil
end
write_attribute ( :storage_quota , val )
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def assign_uuid
2014-07-11 01:22:01 +08:00
self . uuid || = CanvasSlug . generate_securish_uuid
2011-02-01 09:57:29 +08:00
end
protected :assign_uuid
def full_name
name
end
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 = > " / #{ context_url_prefix } /courses/ #{ self . id } " )
end
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
set_policy do
2015-01-09 01:35:09 +08:00
given { | user , session | self . available? && ( self . is_public || ( self . is_public_to_auth_users && session . present? && session . has_key? ( :user_id ) ) ) }
2013-04-06 04:40:05 +08:00
can :read and can :read_outcomes and can :read_syllabus
given { | user | self . available? && self . public_syllabus }
can :read_syllabus
2012-01-04 04:30:49 +08:00
2012-08-17 06:41:43 +08:00
RoleOverride . permissions . each do | permission , details |
2016-02-16 22:56:08 +08:00
given { | user | ( self . active_enrollment_allows ( user , permission , ! details [ :restrict_future_enrollments ] ) || self . account_membership_allows ( user , permission ) ) &&
( ! details [ :if ] || send ( details [ :if ] ) ) }
2011-07-14 00:24:17 +08:00
can permission
2011-02-01 09:57:29 +08:00
end
2012-12-12 21:50:15 +08:00
2013-03-26 03:38:02 +08:00
given { | user , session | session && session [ :enrollment_uuid ] && ( hash = Enrollment . course_user_state ( self , session [ :enrollment_uuid ] ) || { } ) && ( hash [ :enrollment_state ] == " invited " || hash [ :enrollment_state ] == " active " && hash [ :user_state ] . to_s == " pre_registered " ) && ( self . available? || self . completed? || self . claimed? && hash [ :is_admin ] ) }
2012-11-01 04:28:33 +08:00
can :read and can :read_outcomes
refactor user creation/invitations closes #5833
fixes #5573, #5572, #5753
* communication channels are now only unique within a single user
* UserList changes
* Always resolve pseudonym#unique_ids
* Support looking up by SMS CCs
* Option to either require e-mails match an existing CC,
or e-mails that don't match a Pseudonym will always be
returned unattached (relying on better merging behavior
to not have a gazillion accounts created)
* Method to return users, creating new ones (*without* a
Pseudonym) if necessary. (can't create with a pseudonym,
since Pseudonym#unique_id is still unique, I can't have
multiple outstanding users with the same unique_id)
* EnrollmentsFromUserList is mostly gutted, now using UserList's
functionality directy.
* Use UserList for adding account admins, removing the now
unused Account#add_admin => User#find_by_email/User#assert_by_email
codepath
* Update UsersController#create to not worry about duplicate
communication channels
* Remove AccountsController#add_user, and just use
UsersController#create
* Change SIS::UserImporter to send out a merge opportunity
e-mail if a conflicting CC is found (but still create the CC)
* In /profile, don't worry about conflicting CCs (the CC confirmation
process will now allow merging)
* Remove CommunicationChannelsController#try_merge and #merge
* For the non-simple case of CoursesController#enrollment_invitation
redirect to /register (CommunicationsChannelController#confirm)
* Remove CoursesController#transfer_enrollment
* Move PseudonymsController#registration_confirmation to
CommunicationChannelsController#confirm (have to be able to
register an account without a Pseudonym yet)
* Fold the old direct confirm functionality in, if there are
no available merge opportunities
* Allow merging the new account with the currently logged in user
* Allow changing the Pseudonym#unique_id when registering a new
account (since there might be conflicts)
* Display a list of merge opportunities based on conflicting
communication channels
* Provide link(s) to log in as the other user,
redirecting back to the registration page after login is
complete (to complete the merge as the current user)
* Remove several assert_* methods that are no longer needed
* Update PseudonymSessionsController a bit to deal with the new
way of dealing with conflicting CCs (especially CCs from LDAP),
and to redirect back to the registration/confirmation page when
attempting to do a merge
* Expose the open_registration setting; use it to control if
inviting users to a course is able to create new users
Change-Id: If2f38818a71af656854d3bf8431ddbf5dcb84691
Reviewed-on: https://gerrit.instructure.com/6149
Tested-by: Hudson <hudson@instructure.com>
Reviewed-by: Jacob Fugal <jacob@instructure.com>
2011-10-13 04:30:48 +08:00
2015-10-29 22:14:10 +08:00
given { | user | ( self . available? || self . completed? ) && user && user . cached_current_enrollments ( preload_dates : true ) . any? { | e | e . course_id == self . id && [ :active , :invited , :accepted , :completed ] . include? ( e . state_based_on_date ) } }
2012-11-01 04:28:33 +08:00
can :read and can :read_outcomes
2012-01-04 04:30:49 +08:00
2012-03-14 04:08:19 +08:00
# Active students
2013-08-01 07:24:30 +08:00
given { | user |
available? && user &&
2015-10-29 22:14:10 +08:00
user . cached_current_enrollments ( preload_dates : true ) . any? { | e |
2013-08-01 07:24:30 +08:00
e . course_id == id && e . participating_student?
}
}
2012-11-01 04:28:33 +08:00
can :read and can :participate_as_student and can :read_grades and can :read_outcomes
2012-12-12 21:50:15 +08:00
2011-02-11 14:00:39 +08:00
given { | user | ( self . available? || self . completed? ) && user && user . cached_not_ended_enrollments . any? { | e | e . course_id == self . id && e . participating_observer? && e . associated_user_id } }
2011-07-14 00:24:17 +08:00
can :read_grades
2012-01-04 04:30:49 +08:00
2014-06-12 23:44:44 +08:00
given { | user | self . available? && self . teacherless? && user && user . cached_not_ended_enrollments . any? { | e | e . course_id == self . id && e . participating_student? } }
2011-07-14 00:24:17 +08:00
can :update and can :delete and RoleOverride . teacherless_permissions . each { | p | can p }
2011-08-16 06:34:57 +08:00
2012-07-12 01:20:08 +08:00
# Active admins (Teacher/TA/Designer)
2014-06-12 23:44:44 +08:00
given { | user | ( self . available? || self . created? || self . claimed? ) && user && user . cached_not_ended_enrollments . any? { | e | e . course_id == self . id && e . participating_admin? } }
2014-01-11 04:34:56 +08:00
can :read_as_admin and can :read and can :manage and can :update and can :use_student_view and can :read_outcomes and can :view_unpublished_items and can :manage_feature_flags
2011-08-15 23:53:55 +08:00
2012-07-12 01:20:08 +08:00
# Teachers and Designers can delete/reset, but not TAs
2014-06-12 23:44:44 +08:00
given { | user | ! self . deleted? && ! self . sis_source_id && user && user . cached_not_ended_enrollments . any? { | e | e . course_id == self . id && e . participating_content_admin? } }
2011-09-01 05:23:47 +08:00
can :delete
2014-06-12 23:44:44 +08:00
given { | user | ! self . deleted? && user && user . cached_not_ended_enrollments . any? { | e | e . course_id == self . id && e . participating_content_admin? } }
2012-07-12 01:20:08 +08:00
can :reset_content
2011-09-01 05:23:47 +08:00
2012-04-10 07:23:36 +08:00
# Student view student
given { | user | user && user . fake_student? && user . cached_not_ended_enrollments . any? { | e | e . course_id == self . id } }
2012-11-01 04:28:33 +08:00
can :read and can :participate_as_student and can :read_grades and can :read_outcomes
2012-04-10 07:23:36 +08:00
2011-09-01 05:23:47 +08:00
# Prior users
2012-12-12 21:50:15 +08:00
given do | user |
( available? || completed? ) && user &&
2015-03-07 07:17:57 +08:00
prior_enrollments . for_user ( user ) . any? { | e | ! e . inactive? }
2012-12-12 21:50:15 +08:00
end
can :read , :read_outcomes
2011-08-16 06:34:57 +08:00
2012-07-12 01:20:08 +08:00
# Admin (Teacher/TA/Designer) of a concluded course
2012-12-12 21:50:15 +08:00
given do | user |
! self . deleted? && user &&
( prior_enrollments . for_user ( user ) . any? { | e | e . admin? } ||
user . cached_not_ended_enrollments . any? do | e |
e . course_id == self . id && e . admin? && e . completed?
end
)
end
2016-02-02 07:59:22 +08:00
can [ :read , :read_as_admin , :read_roster , :read_prior_roster , :use_student_view , :read_outcomes , :view_unpublished_items ]
2012-12-12 21:50:15 +08:00
2016-02-06 05:03:26 +08:00
# overrideable permissions for concluded users
RoleOverride . concluded_permission_types . each do | permission |
2015-09-08 20:18:39 +08:00
given do | user |
! self . deleted? && user &&
2016-02-06 05:03:26 +08:00
( prior_enrollments . for_user ( user ) . any? { | e | ! e . inactive? && e . has_permission_to? ( permission ) } ||
2015-09-08 20:18:39 +08:00
user . cached_not_ended_enrollments . any? do | e |
2016-02-06 05:03:26 +08:00
e . course_id == self . id && e . completed? && e . has_permission_to? ( permission )
2015-09-08 20:18:39 +08:00
end
)
end
can permission
end
2012-07-12 01:20:08 +08:00
# Teacher or Designer of a concluded course
2012-12-12 21:50:15 +08:00
given do | user |
! self . deleted? && ! self . sis_source_id && user &&
( prior_enrollments . for_user ( user ) . any? { | e | e . content_admin? } ||
user . cached_not_ended_enrollments . any? do | e |
e . course_id == self . id && e . content_admin? && e . state_based_on_date == :completed
end
)
end
2011-09-01 05:23:47 +08:00
can :delete
2011-08-16 06:34:57 +08:00
# Student of a concluded course
2012-12-12 21:50:15 +08:00
given do | user |
( self . available? || self . completed? ) && user &&
2015-03-07 07:17:57 +08:00
( prior_enrollments . for_user ( user ) . any? { | e | ! e . inactive? && ( e . student? || e . assigned_observer? ) } ||
2012-12-12 21:50:15 +08:00
user . cached_not_ended_enrollments . any? do | e |
e . course_id == self . id && ( e . student? || e . assigned_observer? ) && e . state_based_on_date == :completed
end
)
end
2016-02-06 05:03:26 +08:00
can :read , :read_grades , :read_outcomes
2011-08-16 06:34:57 +08:00
# Admin
2014-06-12 23:44:44 +08:00
given { | user | self . account_membership_allows ( user ) }
2015-09-11 22:04:29 +08:00
can :read_as_admin and can :view_unpublished_items
2011-08-16 06:34:57 +08:00
2014-06-12 23:44:44 +08:00
given { | user | self . account_membership_allows ( user , :manage_courses ) }
2014-01-11 04:34:56 +08:00
can :read_as_admin and can :manage and can :update and can :delete and can :use_student_view and can :reset_content and can :view_unpublished_items and can :manage_feature_flags
2011-08-16 06:34:57 +08:00
2014-06-12 23:44:44 +08:00
given { | user | self . account_membership_allows ( user , :read_course_content ) }
2012-11-01 04:28:33 +08:00
can :read and can :read_outcomes
2011-08-16 06:34:57 +08:00
2014-06-12 23:44:44 +08:00
given { | user | ! self . deleted? && self . sis_source_id && self . account_membership_allows ( user , :manage_sis ) }
2011-09-01 05:23:47 +08:00
can :delete
2011-08-16 06:34:57 +08:00
# Admins with read_roster can see prior enrollments (can't just check read_roster directly,
# because students can't see prior enrollments)
2014-06-12 23:44:44 +08:00
given { | user | self . account_membership_allows ( user , :read_roster ) }
2011-08-16 06:34:57 +08:00
can :read_prior_roster
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2013-02-13 04:31:15 +08:00
def allows_gradebook_uploads?
! large_roster?
end
2013-02-13 05:27:09 +08:00
# Public: Determine if SpeedGrader is enabled for the Course.
#
# Returns a boolean.
def allows_speed_grader?
! large_roster?
end
2016-02-16 22:56:08 +08:00
def active_enrollment_allows ( user , permission , allow_future = true )
2011-02-01 09:57:29 +08:00
return false unless user && permission
2011-03-29 00:07:52 +08:00
2011-02-01 09:57:29 +08:00
@enrollment_lookup || = { }
2013-02-09 01:22:24 +08:00
@enrollment_lookup [ user . id ] || = shard . activate do
2014-06-12 23:44:44 +08:00
self . enrollments . active_or_pending . for_user ( user ) . reject { | e | [ :inactive , :completed ] . include? ( e . state_based_on_date ) }
2013-02-09 01:22:24 +08:00
end
2011-03-29 00:07:52 +08:00
2016-02-16 22:56:08 +08:00
@enrollment_lookup [ user . id ] . any? { | e | ( allow_future || e . state_based_on_date == :active ) && e . has_permission_to? ( permission ) }
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def self . find_all_by_context_code ( codes )
ids = codes . map { | c | c . match ( / \ Acourse_( \ d+) \ z / ) [ 1 ] rescue nil } . compact
2015-07-17 05:53:07 +08:00
Course . where ( :id = > ids ) . preload ( :current_enrollments ) . to_a
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2011-02-24 13:26:39 +08:00
def end_at
conclude_at
end
2012-01-04 04:30:49 +08:00
2011-02-24 13:26:39 +08:00
def end_at_changed?
conclude_at_changed?
end
2011-07-28 00:33:04 +08:00
def recently_ended?
conclude_at && conclude_at < Time . now . utc && conclude_at > 1 . month . ago
end
2013-10-11 21:58:14 +08:00
# Public: Return true if the end date for a course (or its term, if the course doesn't have one) has passed.
2012-08-14 06:23:29 +08:00
#
# Returns boolean or nil.
def soft_concluded?
2013-10-11 21:58:14 +08:00
now = Time . now
2014-08-18 09:28:29 +08:00
return end_at < now if end_at && restrict_enrollments_to_course_dates
2013-10-11 21:58:14 +08:00
enrollment_term . end_at && enrollment_term . end_at < now
2012-08-14 06:23:29 +08:00
end
2012-10-26 04:08:44 +08:00
def soft_conclude!
self . conclude_at = Time . now
self . restrict_enrollments_to_course_dates = true
end
2014-04-14 06:09:19 +08:00
def concluded?
completed? || soft_concluded?
end
2012-10-26 04:08:44 +08:00
2016-01-07 13:00:57 +08:00
def account_chain ( include_site_admin : false )
2014-09-11 00:06:39 +08:00
@account_chain || = Account . account_chain ( account_id )
2016-01-07 13:00:57 +08:00
result = @account_chain . dup
Account . add_site_admin_to_chain! ( result ) if include_site_admin
result
2011-09-08 13:20:55 +08:00
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def institution_name
return self . root_account . name if self . root_account_id != Account . default . id
return ( self . account || self . root_account ) . name
end
2011-08-13 00:12:13 +08:00
def account_users_for ( user )
2012-09-18 03:22:39 +08:00
return [ ] unless user
2016-01-07 13:00:57 +08:00
@associated_account_ids || = ( self . associated_accounts + root_account . account_chain ( include_site_admin : true ) ) .
uniq . map { | a | a . active? ? a . id : nil } . compact
2011-08-13 00:12:13 +08:00
@account_users || = { }
2014-03-07 01:10:56 +08:00
@account_users [ user . global_id ] || = Shard . partition_by_shard ( @associated_account_ids ) do | account_chain_ids |
2012-09-18 03:22:39 +08:00
if account_chain_ids == [ Account . site_admin . id ]
Account . site_admin . account_users_for ( user )
else
2015-07-25 00:01:44 +08:00
AccountUser . where ( :account_id = > account_chain_ids , :user_id = > user ) . to_a
2012-09-18 03:22:39 +08:00
end
end
2014-03-07 01:10:56 +08:00
@account_users [ user . global_id ] || = [ ]
@account_users [ user . global_id ]
2011-08-13 00:12:13 +08:00
end
2014-06-12 23:44:44 +08:00
def account_membership_allows ( user , permission = nil )
2011-08-16 06:34:57 +08:00
return false unless user
2011-08-13 00:12:13 +08:00
2011-02-01 09:57:29 +08:00
@membership_allows || = { }
2012-12-07 07:15:53 +08:00
@membership_allows [ [ user . id , permission ] ] || = self . account_users_for ( user ) . any? { | au | permission . nil? || au . has_permission_to? ( self , permission ) }
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 teacherless?
# TODO: I need a better test for teacherless courses... in the mean time we'll just do this
return false
@teacherless_course || = Rails . cache . fetch ( [ 'teacherless_course' , self ] . cache_key ) do
! self . sis_source_id && self . teacher_enrollments . empty?
end
end
2012-01-04 04:30:49 +08:00
2011-11-30 05:59:40 +08:00
def grade_publishing_status_translation ( status , message )
status = " unpublished " if status . blank?
case status
when 'error'
if message . present?
message = t ( 'grade_publishing_status.error_with_message' , " Error: %{message} " , :message = > message )
2011-10-04 02:18:07 +08:00
else
2011-11-30 05:59:40 +08:00
message = t ( 'grade_publishing_status.error' , " Error " )
end
when 'unpublished'
if message . present?
message = t ( 'grade_publishing_status.unpublished_with_message' , " Unpublished: %{message} " , :message = > message )
else
message = t ( 'grade_publishing_status.unpublished' , " Unpublished " )
end
when 'pending'
if message . present?
message = t ( 'grade_publishing_status.pending_with_message' , " Pending: %{message} " , :message = > message )
else
message = t ( 'grade_publishing_status.pending' , " Pending " )
end
when 'publishing'
if message . present?
message = t ( 'grade_publishing_status.publishing_with_message' , " Publishing: %{message} " , :message = > message )
else
message = t ( 'grade_publishing_status.publishing' , " Publishing " )
end
when 'published'
if message . present?
message = t ( 'grade_publishing_status.published_with_message' , " Published: %{message} " , :message = > message )
else
message = t ( 'grade_publishing_status.published' , " Published " )
end
when 'unpublishable'
if message . present?
message = t ( 'grade_publishing_status.unpublishable_with_message' , " Unpublishable: %{message} " , :message = > message )
else
message = t ( 'grade_publishing_status.unpublishable' , " Unpublishable " )
end
else
if message . present?
message = t ( 'grade_publishing_status.unknown_with_message' , " Unknown status, %{status}: %{message} " , :message = > message , :status = > status )
else
message = t ( 'grade_publishing_status.unknown' , " Unknown status, %{status} " , :status = > status )
2011-10-04 02:18:07 +08:00
end
end
2011-11-30 05:59:40 +08:00
message
2011-10-01 02:40:25 +08:00
end
2011-11-30 05:59:40 +08:00
def grade_publishing_statuses
found_statuses = [ ] . to_set
2012-03-14 04:08:19 +08:00
enrollments = student_enrollments . not_fake . group_by do | e |
2011-11-30 05:59:40 +08:00
found_statuses . add e . grade_publishing_status
grade_publishing_status_translation ( e . grade_publishing_status , e . grade_publishing_message )
2011-04-15 07:49:25 +08:00
end
2011-11-30 05:59:40 +08:00
overall_status = " error "
overall_status = " unpublished " unless found_statuses . size > 0
overall_status = ( %w{ error unpublished pending publishing published unpublishable } ) . detect { | s | found_statuses . include? ( s ) } || overall_status
return enrollments , overall_status
end
def should_kick_off_grade_publishing_timeout?
settings = Canvas :: Plugin . find! ( 'grade_export' ) . settings
settings [ :success_timeout ] . to_i > 0 && Canvas :: Plugin . value_to_boolean ( settings [ :wait_for_success ] )
end
def self . valid_grade_export_types
@valid_grade_export_types || = {
" instructure_csv " = > {
:name = > t ( 'grade_export_types.instructure_csv' , " Instructure formatted CSV " ) ,
:callback = > lambda { | course , enrollments , publishing_user , publishing_pseudonym |
course . generate_grade_publishing_csv_output ( enrollments , publishing_user , publishing_pseudonym )
} ,
:requires_grading_standard = > false ,
:requires_publishing_pseudonym = > false
}
}
end
def allows_grade_publishing_by ( user )
return false unless Canvas :: Plugin . find! ( 'grade_export' ) . enabled?
settings = Canvas :: Plugin . find! ( 'grade_export' ) . settings
format_settings = Course . valid_grade_export_types [ settings [ :format_type ] ]
return false unless format_settings
2015-06-23 05:47:04 +08:00
return false if SisPseudonym . for ( user , self ) . nil? && format_settings [ :requires_publishing_pseudonym ]
2011-11-30 05:59:40 +08:00
return true
2011-04-15 07:49:25 +08:00
end
2013-10-04 03:47:36 +08:00
def publish_final_grades ( publishing_user , user_ids_to_publish = nil )
2011-04-15 07:49:25 +08:00
# we want to set all the publishing statuses to 'pending' immediately,
# and then as a delayed job, actually go publish them.
2011-11-30 05:59:40 +08:00
raise " final grade publishing disabled " unless Canvas :: Plugin . find! ( 'grade_export' ) . enabled?
settings = Canvas :: Plugin . find! ( 'grade_export' ) . settings
2011-04-15 07:49:25 +08:00
last_publish_attempt_at = Time . now . utc
2013-10-04 03:47:36 +08:00
scope = self . student_enrollments . not_fake
scope = scope . where ( user_id : user_ids_to_publish ) if user_ids_to_publish
scope . update_all ( :grade_publishing_status = > " pending " ,
2015-01-01 05:12:15 +08:00
:grade_publishing_message = > nil ,
:last_publish_attempt_at = > last_publish_attempt_at )
2011-04-15 07:49:25 +08:00
2013-10-04 03:47:36 +08:00
send_later_if_production ( :send_final_grades_to_endpoint , publishing_user , user_ids_to_publish )
2011-11-30 05:59:40 +08:00
send_at ( last_publish_attempt_at + settings [ :success_timeout ] . to_i . seconds , :expire_pending_grade_publishing_statuses , last_publish_attempt_at ) if should_kick_off_grade_publishing_timeout?
2011-04-15 07:49:25 +08:00
end
2013-10-04 03:47:36 +08:00
def send_final_grades_to_endpoint ( publishing_user , user_ids_to_publish = nil )
2011-04-15 07:49:25 +08:00
# actual grade publishing logic is here, but you probably want
# 'publish_final_grades'
2013-10-04 03:47:36 +08:00
self . recompute_student_scores_without_send_later ( user_ids_to_publish )
2015-07-17 05:53:07 +08:00
enrollments = self . student_enrollments . not_fake . eager_load ( :user ) . preload ( :course_section ) . order_by_sortable_name
2013-10-04 03:47:36 +08:00
enrollments = enrollments . where ( user_id : user_ids_to_publish ) if user_ids_to_publish
2011-04-15 07:49:25 +08:00
2011-11-30 05:59:40 +08:00
errors = [ ]
posts_to_make = [ ]
posted_enrollment_ids = [ ]
all_enrollment_ids = enrollments . map ( & :id )
2011-06-22 02:58:04 +08:00
begin
2011-04-15 07:49:25 +08:00
2011-11-30 05:59:40 +08:00
raise " final grade publishing disabled " unless Canvas :: Plugin . find! ( 'grade_export' ) . enabled?
settings = Canvas :: Plugin . find! ( 'grade_export' ) . settings
2011-06-22 02:58:04 +08:00
raise " endpoint undefined " if settings [ :publish_endpoint ] . blank?
2011-11-30 05:59:40 +08:00
format_settings = Course . valid_grade_export_types [ settings [ :format_type ] ]
raise " unknown format type: #{ settings [ :format_type ] } " unless format_settings
raise " grade publishing requires a grading standard " if ! self . grading_standard_enabled? && format_settings [ :requires_grading_standard ]
2011-06-22 02:58:04 +08:00
2015-06-23 05:47:04 +08:00
publishing_pseudonym = SisPseudonym . for ( publishing_user , self )
2011-11-30 05:59:40 +08:00
raise " publishing disallowed for this publishing user " if publishing_pseudonym . nil? and format_settings [ :requires_publishing_pseudonym ]
2011-06-22 02:58:04 +08:00
2011-11-30 05:59:40 +08:00
callback = Course . valid_grade_export_types [ settings [ :format_type ] ] [ :callback ]
2011-06-22 02:58:04 +08:00
2011-11-30 05:59:40 +08:00
posts_to_make = callback . call ( self , enrollments , publishing_user , publishing_pseudonym )
2011-06-22 02:58:04 +08:00
2011-11-30 05:59:40 +08:00
rescue = > e
2013-03-19 03:07:47 +08:00
Enrollment . where ( :id = > all_enrollment_ids ) . update_all ( :grade_publishing_status = > " error " , :grade_publishing_message = > e . to_s )
2011-11-30 05:59:40 +08:00
raise e
2011-04-28 08:16:51 +08:00
end
2011-04-15 07:49:25 +08:00
2013-10-22 03:39:44 +08:00
posts_to_make . each do | enrollment_ids , res , mime_type , headers = { } |
2011-04-28 08:16:51 +08:00
begin
2011-11-30 05:59:40 +08:00
posted_enrollment_ids += enrollment_ids
2013-10-25 03:58:46 +08:00
if res
SSLCommon . post_data ( settings [ :publish_endpoint ] , res , mime_type , headers )
end
2013-03-19 03:07:47 +08:00
Enrollment . where ( :id = > enrollment_ids ) . update_all ( :grade_publishing_status = > ( should_kick_off_grade_publishing_timeout? ? " publishing " : " published " ) , :grade_publishing_message = > nil )
2011-04-28 08:16:51 +08:00
rescue = > e
errors << e
2013-03-19 03:07:47 +08:00
Enrollment . where ( :id = > enrollment_ids ) . update_all ( :grade_publishing_status = > " error " , :grade_publishing_message = > e . to_s )
2011-04-28 08:16:51 +08:00
end
end
2011-06-22 02:58:04 +08:00
2013-03-19 03:07:47 +08:00
Enrollment . where ( :id = > ( all_enrollment_ids . to_set - posted_enrollment_ids . to_set ) . to_a ) . update_all ( :grade_publishing_status = > " unpublishable " , :grade_publishing_message = > nil )
2011-11-30 05:59:40 +08:00
2011-04-28 08:16:51 +08:00
raise errors [ 0 ] if errors . size > 0
end
2012-01-04 04:30:49 +08:00
2011-11-30 05:59:40 +08:00
def generate_grade_publishing_csv_output ( enrollments , publishing_user , publishing_pseudonym )
2011-04-28 08:16:51 +08:00
enrollment_ids = [ ]
2013-04-19 23:54:35 +08:00
res = CSV . generate do | csv |
2012-05-09 11:25:52 +08:00
row = [ " publisher_id " , " publisher_sis_id " , " course_id " , " course_sis_id " , " section_id " , " section_sis_id " , " student_id " , " student_sis_id " , " enrollment_id " , " enrollment_status " , " score " ]
2011-11-30 05:59:40 +08:00
row << " grade " if self . grading_standard_enabled?
csv << row
2011-04-15 07:49:25 +08:00
enrollments . each do | enrollment |
2011-06-23 04:35:05 +08:00
next unless enrollment . computed_final_score
2011-11-30 05:59:40 +08:00
enrollment_ids << enrollment . id
2014-09-12 03:44:34 +08:00
pseudonym_sis_ids = enrollment . user . pseudonyms . active . where ( account_id : self . root_account_id ) . pluck ( :sis_user_id )
2011-11-30 05:59:40 +08:00
pseudonym_sis_ids = [ nil ] if pseudonym_sis_ids . empty?
pseudonym_sis_ids . each do | pseudonym_sis_id |
2012-05-09 11:25:52 +08:00
row = [ publishing_user . try ( :id ) , publishing_pseudonym . try ( :sis_user_id ) ,
enrollment . course . id , enrollment . course . sis_source_id ,
enrollment . course_section . id , enrollment . course_section . sis_source_id ,
enrollment . user . id , pseudonym_sis_id , enrollment . id ,
enrollment . workflow_state , enrollment . computed_final_score ]
2011-11-30 05:59:40 +08:00
row << enrollment . computed_final_grade if self . grading_standard_enabled?
csv << row
2011-04-15 07:49:25 +08:00
end
end
end
2011-11-30 05:59:40 +08:00
return [ [ enrollment_ids , res , " text/csv " ] ]
2011-04-15 07:49:25 +08:00
end
def expire_pending_grade_publishing_statuses ( last_publish_attempt_at )
2013-03-19 03:07:47 +08:00
self . student_enrollments . not_fake . where ( :grade_publishing_status = > [ 'pending' , 'publishing' ] ,
:last_publish_attempt_at = > last_publish_attempt_at ) .
update_all ( :grade_publishing_status = > 'error' , :grade_publishing_message = > " Timed out. " )
2011-04-15 07:49:25 +08:00
end
2015-07-17 05:53:07 +08:00
2015-04-21 00:56:13 +08:00
def gradebook_to_csv_in_background ( filename , user , options = { } )
2015-05-19 01:33:29 +08:00
progress = progresses . build ( tag : 'gradebook_to_csv' )
2015-04-21 00:56:13 +08:00
progress . save!
2015-05-13 23:06:20 +08:00
exported_gradebook = gradebook_csvs . where ( user_id : user ) . first_or_initialize
2015-04-21 00:56:13 +08:00
attachment = user . attachments . build
attachment . filename = filename
attachment . content_type = 'text/csv'
attachment . file_state = 'hidden'
attachment . save!
exported_gradebook . attachment = attachment
exported_gradebook . progress = progress
exported_gradebook . save!
2015-07-31 08:34:24 +08:00
progress . process_job (
self ,
:generate_csv ,
{ preserve_method_args : true } ,
2015-08-27 07:38:05 +08:00
user ,
options ,
2015-07-31 08:34:24 +08:00
attachment
)
2015-04-21 00:56:13 +08:00
{ attachment_id : attachment . id , progress_id : progress . id }
end
2015-08-27 07:38:05 +08:00
def generate_csv ( user , options , attachment )
csv = GradebookExporter . new ( self , user , options ) . to_csv
2015-04-21 00:56:13 +08:00
create_attachment ( attachment , csv )
end
2011-12-07 07:36:27 +08:00
2015-04-21 00:56:13 +08:00
def create_attachment ( attachment , csv )
attachment . uploaded_data = StringIO . new ( csv )
2015-05-17 07:07:02 +08:00
attachment . content_type = 'text/csv'
2015-04-21 00:56:13 +08:00
attachment . save!
end
2011-12-07 07:36:27 +08:00
# included to make it easier to work with api, which returns
# sis_source_id as sis_course_id.
alias_attribute :sis_course_id , :sis_source_id
2011-04-09 06:45:38 +08:00
def grading_standard_title
2011-11-30 05:59:40 +08:00
if self . grading_standard_enabled?
2011-06-22 00:23:08 +08:00
self . grading_standard . try ( :title ) || t ( 'default_grading_scheme_name' , " Default Grading Scheme " )
2011-04-09 06:45:38 +08:00
else
nil
end
end
2012-01-04 04:30:49 +08:00
2011-04-09 06:45:38 +08:00
def score_to_grade ( score )
2011-11-30 05:59:40 +08:00
return nil unless self . grading_standard_enabled? && score
2014-02-05 01:57:32 +08:00
if grading_standard
grading_standard . score_to_grade ( score )
else
GradingStandard . default_instance . score_to_grade ( score )
end
2011-04-09 06:45:38 +08:00
end
2011-02-01 09:57:29 +08:00
2014-12-16 08:41:42 +08:00
def active_course_level_observers
participating_observers . observing_full_course ( self . id )
end
def participants ( include_observers = false , opts = { } )
participants = [ ]
participants += participating_admins
applicable_students = if opts [ :excluded_user_ids ]
participating_students . reject { | p | opts [ :excluded_user_ids ] . include? ( p . id ) }
else
participating_students
end
participants += applicable_students
if include_observers
participants += User . observing_students_in_course ( applicable_students . map ( & :id ) , self . id )
participants += User . observing_full_course ( self . id )
end
participants . uniq
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
def enroll_user ( user , type = 'StudentEnrollment' , opts = { } )
2011-02-01 09:57:29 +08:00
enrollment_state = opts [ :enrollment_state ]
2015-11-05 06:04:40 +08:00
enrollment_state || = 'active' if type == 'ObserverEnrollment'
2011-02-01 09:57:29 +08:00
section = opts [ :section ]
2011-12-09 08:02:47 +08:00
limit_privileges_to_course_section = opts [ :limit_privileges_to_course_section ]
2012-04-16 23:27:28 +08:00
associated_user_id = opts [ :associated_user_id ]
2014-09-08 20:48:45 +08:00
role = opts [ :role ] || Enrollment . get_built_in_role_for_type ( type )
2014-02-14 06:44:47 +08:00
start_at = opts [ :start_at ]
end_at = opts [ :end_at ]
2013-09-11 01:45:13 +08:00
self_enrolled = opts [ :self_enrolled ]
2011-02-01 09:57:29 +08:00
section || = self . default_section
enrollment_state || = self . available? ? " invited " : " creation_pending "
2012-06-19 05:38:29 +08:00
if type == 'TeacherEnrollment' || type == 'TaEnrollment' || type == 'DesignerEnrollment'
enrollment_state = 'invited' if enrollment_state == 'creation_pending'
else
enrollment_state = 'creation_pending' if enrollment_state == 'invited' && ! self . available?
end
2014-04-30 23:25:02 +08:00
Course . unique_constraint_retry do
if opts [ :allow_multiple_enrollments ]
2014-09-08 20:48:45 +08:00
e = self . all_enrollments . where ( user_id : user , type : type , role_id : role . id , associated_user_id : associated_user_id , course_section_id : section . id ) . first
2014-04-30 23:25:02 +08:00
else
# order by course_section_id<>section.id so that if there *is* an existing enrollment for this section, we get it (false orders before true)
e = self . all_enrollments .
2014-09-08 20:48:45 +08:00
where ( user_id : user , type : type , role_id : role . id , associated_user_id : associated_user_id ) .
2014-04-02 05:50:27 +08:00
order ( " course_section_id<> #{ section . id } " ) .
first
2014-04-30 23:25:02 +08:00
end
2015-01-30 01:15:09 +08:00
if e && ( ! e . active? || opts [ :force_update ] )
2014-04-30 23:25:02 +08:00
e . already_enrolled = true
2014-09-04 11:31:10 +08:00
if e . workflow_state == 'deleted'
e . sis_batch_id = nil
e . sis_source_id = nil
end
2014-04-30 23:25:02 +08:00
e . attributes = {
:course_section = > section ,
2015-10-30 04:19:00 +08:00
:workflow_state = > e . is_a? ( StudentViewEnrollment ) ? 'active' : enrollment_state
} if e . completed? || e . rejected? || e . deleted? || e . workflow_state != enrollment_state
2014-04-30 23:25:02 +08:00
end
2015-10-30 04:19:00 +08:00
# if we're reusing an enrollment and +limit_privileges_to_course_section+ was supplied, apply it
e . limit_privileges_to_course_section = limit_privileges_to_course_section if e unless limit_privileges_to_course_section . nil?
2014-04-30 23:25:02 +08:00
# if we're creating a new enrollment, we want to return it as the correct
# subclass, but without using associations, we need to manually activate
# sharding. We should probably find a way to go back to using the
# association here -- just ran out of time.
self . shard . activate do
e || = Enrollment . typed_enrollment ( type ) . new (
:user = > user ,
:course = > self ,
:course_section = > section ,
:workflow_state = > enrollment_state ,
:limit_privileges_to_course_section = > limit_privileges_to_course_section )
end
e . associated_user_id = associated_user_id
2014-09-08 20:48:45 +08:00
e . role = role
2014-04-30 23:25:02 +08:00
e . self_enrolled = self_enrolled
e . start_at = start_at
e . end_at = end_at
if e . changed?
transaction do
2015-05-02 03:21:40 +08:00
# without this, inserting/updating on enrollments will share lock the course, but then
# it tries to touch the course, which will deadlock with another transaction doing the
2015-05-12 00:32:15 +08:00
# same thing.
self . lock! ( :no_key_update )
2014-04-30 23:25:02 +08:00
if opts [ :no_notify ]
e . save_without_broadcasting
else
e . save
end
2014-01-10 03:30:03 +08:00
end
2011-10-27 00:39:39 +08:00
end
2014-04-30 23:25:02 +08:00
e . user = user
self . claim if self . created? && e && e . admin?
unless opts [ :skip_touch_user ]
e . associated_user . try ( :touch )
user . touch
end
user . reload
e
2011-10-27 00:39:39 +08:00
end
2013-02-12 04:41:18 +08:00
end
2012-01-04 04:30:49 +08:00
2011-10-27 00:39:39 +08:00
def enroll_student ( user , opts = { } )
enroll_user ( user , 'StudentEnrollment' , opts )
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2012-05-30 07:35:00 +08:00
def self_enroll_student ( user , opts = { } )
2013-09-11 01:45:13 +08:00
enrollment = enroll_student ( user , opts . merge ( :self_enrolled = > true ) )
2012-12-07 14:28:37 +08:00
enrollment . accept ( :force )
2012-05-30 07:35:00 +08:00
unless opts [ :skip_pseudonym ]
new_pseudonym = user . find_or_initialize_pseudonym_for_account ( root_account )
new_pseudonym . save if new_pseudonym && new_pseudonym . changed?
end
enrollment
end
2014-02-28 01:46:02 +08:00
def enroll_ta ( user , opts = { } )
enroll_user ( user , 'TaEnrollment' , opts )
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2014-02-28 01:46:02 +08:00
def enroll_designer ( user , opts = { } )
enroll_user ( user , 'DesignerEnrollment' , opts )
2012-01-13 07:57:58 +08:00
end
2014-02-28 01:46:02 +08:00
def enroll_teacher ( user , opts = { } )
enroll_user ( user , 'TeacherEnrollment' , opts )
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2013-01-31 03:39:40 +08:00
def resubmission_for ( asset )
2014-07-24 01:14:22 +08:00
asset . ignores . where ( :purpose = > 'grading' , :permanent = > false ) . delete_all
2014-01-20 23:57:14 +08:00
instructors . order ( :id ) . each ( & :touch )
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2011-04-09 06:45:38 +08:00
def grading_standard_enabled
! ! self . grading_standard_id
end
2011-11-30 05:59:40 +08:00
alias_method :grading_standard_enabled? , :grading_standard_enabled
2012-01-04 04:30:49 +08:00
2011-04-09 06:45:38 +08:00
def grading_standard_enabled = ( val )
2011-11-30 05:59:40 +08:00
if Canvas :: Plugin . value_to_boolean ( val )
2011-04-09 06:45:38 +08:00
self . grading_standard_id || = 0
2011-11-30 05:59:40 +08:00
else
self . grading_standard = self . grading_standard_id = nil
2011-04-09 06:45:38 +08:00
end
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def readable_default_wiki_editing_roles
roles = self . default_wiki_editing_roles || " teachers "
case roles
when 'teachers'
2011-06-22 00:23:08 +08:00
t ( 'wiki_permissions.only_teachers' , 'Only Teachers' )
2011-02-01 09:57:29 +08:00
when 'teachers,students'
2011-06-22 00:23:08 +08:00
t ( 'wiki_permissions.teachers_students' , 'Teacher and Students' )
2011-02-01 09:57:29 +08:00
when 'teachers,students,public'
2011-06-22 00:23:08 +08:00
t ( 'wiki_permissions.all' , 'Anyone' )
2011-02-01 09:57:29 +08:00
else
2011-06-22 00:23:08 +08:00
t ( 'wiki_permissions.only_teachers' , 'Only Teachers' )
2011-02-01 09:57:29 +08:00
end
end
2012-01-04 04:30:49 +08:00
2012-07-11 00:57:14 +08:00
def default_section ( opts = { } )
2014-09-12 03:44:34 +08:00
section = course_sections . active . where ( default_section : true ) . first
2012-07-11 00:57:14 +08:00
if ! section && opts [ :include_xlists ]
2013-03-19 03:07:47 +08:00
section = CourseSection . active . where ( :nonxlist_course_id = > self ) . order ( :id ) . first
2012-07-11 00:57:14 +08:00
end
2014-01-24 01:16:28 +08:00
if ! section && ! opts [ :no_create ]
2012-07-11 00:57:14 +08:00
section = course_sections . build
section . default_section = true
2011-05-07 02:22:03 +08:00
section . course = self
2013-01-29 06:26:26 +08:00
section . root_account_id = self . root_account_id
2014-01-09 02:11:04 +08:00
Shackles . activate ( :master ) do
section . save unless new_record?
end
2011-05-07 02:22:03 +08:00
end
2012-07-11 00:57:14 +08:00
section
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 assert_section
if self . course_sections . active . empty?
default = self . default_section
default . workflow_state = 'active'
default . save
end
end
2012-01-04 04:30:49 +08:00
2013-09-28 03:07:21 +08:00
def file_structure_for ( user )
User . file_structure_for ( self , user )
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def self . copy_authorized_content ( html , to_context , user )
return html unless to_context
pairs = [ ]
content_types_to_copy = [ 'files' ]
matches = html . scan ( / \/ (courses|groups|users) \/ ( \ d+) \/ ( \ w+) / ) do | match |
pairs << [ match [ 0 ] . singularize , match [ 1 ] . to_i ] if content_types_to_copy . include? ( match [ 2 ] )
end
pairs = pairs . select { | p | p [ 0 ] != to_context . class . to_s || p [ 1 ] != to_context . id }
pairs . uniq . each do | context_type , id |
context = Context . find_by_asset_string ( " #{ context_type } _ #{ id } " ) rescue nil
if context
2015-05-06 03:20:56 +08:00
next if context . respond_to? ( :context ) && to_context == context . context
2013-09-23 22:18:58 +08:00
next if to_context . respond_to? ( :context ) && context == to_context . context
2015-05-06 03:20:56 +08:00
2014-05-03 00:35:29 +08:00
if context . grants_right? ( user , :manage_content )
2011-02-01 09:57:29 +08:00
html = self . migrate_content_links ( html , context , to_context , content_types_to_copy )
2012-01-04 04:30:49 +08:00
else
2011-02-01 09:57:29 +08:00
html = self . migrate_content_links ( html , context , to_context , content_types_to_copy , user )
end
end
end
html
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def turnitin_settings
# check if somewhere up the account chain turnitin is enabled and
# has valid settings
2013-07-09 03:08:06 +08:00
account . turnitin_settings
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 turnitin_pledge
self . account . closest_turnitin_pledge
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def all_turnitin_comments
comments = self . account . closest_turnitin_comments || " "
if self . turnitin_comments && ! self . turnitin_comments . empty?
comments += " \n \n " if comments && ! comments . empty?
comments += self . turnitin_comments
end
self . extend TextHelper
format_message ( comments ) . first
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def turnitin_enabled?
! ! self . turnitin_settings
end
2011-04-28 02:34:01 +08:00
2011-02-01 09:57:29 +08:00
def self . migrate_content_links ( html , from_context , to_context , supported_types = nil , user_to_check_for_permission = nil )
2011-10-04 23:03:19 +08:00
return html unless html . present? && to_context
from_name = from_context . class . name . tableize
to_name = to_context . class . name . tableize
2011-02-01 09:57:29 +08:00
@merge_mappings || = { }
2011-10-04 23:03:19 +08:00
rewriter = UserContent :: HtmlRewriter . new ( from_context , user_to_check_for_permission )
2011-02-01 09:57:29 +08:00
limit_migrations_to_listed_types = ! ! supported_types
2011-10-04 23:03:19 +08:00
rewriter . allowed_types = %w( assignments calendar_events discussion_topics collaborations files conferences quizzes groups modules )
rewriter . set_default_handler do | match |
new_url = match . url
2011-10-16 06:43:54 +08:00
next ( new_url ) if supported_types && ! supported_types . include? ( match . type )
2013-09-23 22:18:58 +08:00
if match . obj_id
new_id = @merge_mappings [ " #{ match . obj_class . name . underscore } _ #{ match . obj_id } " ]
2014-09-12 03:44:34 +08:00
next ( new_url ) unless rewriter . user_can_view_content? { match . obj_class . where ( id : match . obj_id ) . first }
2013-09-23 22:18:58 +08:00
if ! limit_migrations_to_listed_types || new_id
new_url = new_url . gsub ( " #{ match . type } / #{ match . obj_id } " , new_id ? " #{ match . type } / #{ new_id } " : " #{ match . type } " )
end
2011-02-01 09:57:29 +08:00
end
2011-10-04 23:03:19 +08:00
new_url . gsub ( " / #{ from_name } / #{ from_context . id } " , " / #{ to_name } / #{ to_context . id } " )
end
rewriter . set_unknown_handler do | match |
match . url . gsub ( " / #{ from_name } / #{ from_context . id } " , " / #{ to_name } / #{ to_context . id } " )
2011-02-01 09:57:29 +08:00
end
2011-10-04 23:03:19 +08:00
html = rewriter . translate_content ( html )
2011-02-01 09:57:29 +08:00
if ! limit_migrations_to_listed_types
2011-10-04 23:03:19 +08:00
# for things like calendar urls, swap out the old context id with the new one
2011-02-01 09:57:29 +08:00
regex = Regexp . new ( " include_contexts=[^ \\ s&]* #{ from_context . asset_string } " )
html = html . gsub ( regex ) do | match |
match . gsub ( " #{ from_context . asset_string } " , " #{ to_context . asset_string } " )
end
2011-10-04 23:03:19 +08:00
# swap out the old host with the new host
2011-02-01 09:57:29 +08:00
html = html . gsub ( HostUrl . context_host ( from_context ) , HostUrl . context_host ( to_context ) )
end
2011-10-04 23:03:19 +08:00
2011-02-01 09:57:29 +08:00
html
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def migrate_content_links ( html , from_course )
Course . migrate_content_links ( html , from_course , self )
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
attr_accessor :merge_results
def log_merge_result ( text )
@merge_results || = [ ]
logger . debug text
@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 )
log_merge_result ( text )
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def bool_res ( val )
2011-11-30 05:59:40 +08:00
Canvas :: Plugin . value_to_boolean ( val )
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2014-06-02 23:59:13 +08:00
attr_accessor :full_migration_hash , :external_url_hash ,
2015-06-04 21:51:57 +08:00
:folder_name_lookups , :assignment_group_no_drop_assignments , :migration_results
2014-04-22 23:41:03 +08:00
2011-02-01 09:57:29 +08:00
def backup_to_json
backup . to_json
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def backup
res = [ ]
res += self . folders . active
res += self . attachments . active
res += self . assignment_groups . active
res += self . assignments . active
res += self . submissions
res += self . quizzes
res += self . discussion_topics . active
res += self . discussion_entries . active
res += self . wiki . wiki_pages . active
res += self . calendar_events . active
res
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def map_merge ( old_item , new_item )
@merge_mappings || = { }
@merge_mappings [ old_item . asset_string ] = new_item && new_item . id
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def merge_mapped_id ( old_item )
@merge_mappings || = { }
return nil unless old_item
return @merge_mappings [ old_item ] if old_item . is_a? ( String )
@merge_mappings [ old_item . asset_string ]
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def same_dates? ( old , new , columns )
old && new && columns . all? { | column |
old . respond_to? ( column ) && new . respond_to? ( column ) && old . send ( column ) == new . send ( column )
}
end
2012-01-04 04:30:49 +08:00
2012-03-28 22:52:21 +08:00
def copy_attachments_from_course ( course , options = { } )
2012-04-24 07:52:12 +08:00
root_folder = Folder . root_folders ( self ) . first
root_folder_name = root_folder . name + '/'
2012-03-28 22:52:21 +08:00
ce = options [ :content_export ]
cm = options [ :content_migration ]
2015-07-25 00:01:44 +08:00
attachments = course . attachments . where ( " file_state <> 'deleted' " ) . to_a
2012-03-28 22:52:21 +08:00
total = attachments . count + 1
2013-02-28 23:02:22 +08:00
Attachment . skip_media_object_creation do
attachments . each_with_index do | file , i |
cm . update_import_progress ( ( i . to_f / total ) * 18 . 0 ) if cm && ( i % 10 == 0 )
if ! ce || ce . export_object? ( file )
begin
new_file = file . clone_for ( self , nil , :overwrite = > true )
2015-06-04 21:51:57 +08:00
cm . add_attachment_path ( file . full_display_path . gsub ( / \ A #{ root_folder_name } / , '' ) , new_file . migration_id )
2013-02-28 23:02:22 +08:00
new_folder_id = merge_mapped_id ( file . folder )
2012-04-24 07:52:12 +08:00
2013-02-28 23:02:22 +08:00
if file . folder && file . folder . parent_folder_id . nil?
new_folder_id = root_folder . id
2012-03-28 22:52:21 +08:00
end
2013-02-28 23:02:22 +08:00
# make sure the file has somewhere to go
if ! new_folder_id
# gather mapping of needed folders from old course to new course
old_folders = [ ]
old_folders << file . folder
new_folders = [ ]
new_folders << old_folders . last . clone_for ( self , nil , options . merge ( { :include_subcontent = > false } ) )
while old_folders . last . parent_folder && old_folders . last . parent_folder . parent_folder_id
old_folders << old_folders . last . parent_folder
new_folders << old_folders . last . clone_for ( self , nil , options . merge ( { :include_subcontent = > false } ) )
2013-01-11 04:10:15 +08:00
end
2013-02-28 23:02:22 +08:00
old_folders . reverse!
new_folders . reverse!
# try to use folders that already match if possible
final_new_folders = [ ]
parent_folder = Folder . root_folders ( self ) . first
old_folders . each_with_index do | folder , idx |
2014-09-12 03:44:34 +08:00
if f = parent_folder . active_sub_folders . where ( name : folder . name ) . first
2013-02-28 23:02:22 +08:00
final_new_folders << f
else
final_new_folders << new_folders [ idx ]
end
parent_folder = final_new_folders . last
end
# add or update the folder structure needed for the file
final_new_folders . first . parent_folder_id || =
merge_mapped_id ( old_folders . first . parent_folder ) ||
Folder . root_folders ( self ) . first . id
old_folders . each_with_index do | folder , idx |
final_new_folders [ idx ] . save!
map_merge ( folder , final_new_folders [ idx ] )
final_new_folders [ idx + 1 ] . parent_folder_id || = final_new_folders [ idx ] . id if final_new_folders [ idx + 1 ]
end
new_folder_id = merge_mapped_id ( file . folder )
2013-01-11 04:10:15 +08:00
end
2013-02-28 23:02:22 +08:00
new_file . folder_id = new_folder_id
new_file . save_without_broadcasting!
2015-08-06 01:27:48 +08:00
cm . add_imported_item ( new_file )
2013-02-28 23:02:22 +08:00
map_merge ( file , new_file )
rescue
cm . add_warning ( t ( :file_copy_error , " Couldn't copy file \" %{name} \" " , :name = > file . display_name || file . path_name ) , $! )
2012-03-28 22:52:21 +08:00
end
end
end
end
end
2011-02-01 09:57:29 +08:00
def self . clonable_attributes
2015-03-12 02:22:02 +08:00
[ :group_weighting_scheme , :grading_standard_id , :is_public , :public_syllabus ,
2013-05-21 03:39:36 +08:00
:allow_student_wiki_edits , :show_public_context_messages ,
2015-04-09 05:51:35 +08:00
:syllabus_body , :allow_student_forum_attachments , :lock_all_announcements ,
2011-05-03 04:29:32 +08:00
:default_wiki_editing_roles , :allow_student_organized_groups ,
2014-04-15 02:48:44 +08:00
:default_view , :show_total_grade_as_points ,
:show_all_discussion_entries , :open_enrollment ,
2011-05-03 04:29:32 +08:00
:storage_quota , :tab_configuration , :allow_wiki_comments ,
2013-02-08 03:56:05 +08:00
:turnitin_comments , :self_enrollment , :license , :indexed , :locale ,
:hide_final_grade , :hide_distribution_graphs ,
2015-07-21 05:19:44 +08:00
:allow_student_discussion_topics , :allow_student_discussion_editing , :lock_all_announcements ,
:organize_epub_by_content_type ]
2011-02-01 09:57:29 +08:00
end
2011-05-03 04:29:32 +08:00
2013-09-11 03:07:23 +08:00
def set_course_dates_if_blank ( shift_options )
2015-07-24 21:36:15 +08:00
unless Canvas :: Plugin . value_to_boolean ( shift_options [ :remove_dates ] )
2014-07-29 01:22:01 +08:00
self . start_at || = shift_options [ :default_start_at ]
self . conclude_at || = shift_options [ :default_conclude_at ]
end
2013-09-11 03:07:23 +08:00
end
2011-02-01 09:57:29 +08:00
def real_start_date
return self . start_at . to_date if self . start_at
all_dates . min
end
def all_dates
( self . calendar_events . active + self . assignments . active ) . inject ( [ ] ) { | list , e |
list << e . end_at if e . end_at
list << e . start_at if e . start_at
list
} . compact . flatten . map { | d | d . to_date } . uniq rescue [ ]
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def real_end_date
return self . conclude_at . to_date if self . conclude_at
all_dates . max
end
def is_a_context?
true
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def self . serialization_excludes ; [ :uuid ] ; end
2012-01-04 04:30:49 +08:00
2016-02-04 01:50:29 +08:00
def section_visibilities_for ( user , opts = { } )
2015-09-10 21:38:06 +08:00
RequestCache . cache ( 'section_visibilities_for' , user , self ) do
2013-01-30 05:49:16 +08:00
shard . activate do
Rails . cache . fetch ( [ 'section_visibilities_for' , user , self ] . cache_key ) do
2016-02-04 01:50:29 +08:00
workflow_not = opts [ :excluded_workflows ] || 'deleted'
enrollments = Enrollment . select ( [
:course_section_id ,
:limit_privileges_to_course_section ,
:type ,
:associated_user_id ] )
. where ( " user_id=? AND course_id=? " , user , self )
. where . not ( workflow_state : workflow_not )
2013-01-30 05:49:16 +08:00
enrollments . map do | e |
{
:course_section_id = > e . course_section_id ,
:limit_privileges_to_course_section = > e . limit_privileges_to_course_section ,
:type = > e . type ,
:associated_user_id = > e . associated_user_id ,
:admin = > e . admin?
}
end
end
2011-02-01 09:57:29 +08:00
end
2015-09-10 21:38:06 +08:00
end
2011-08-19 06:03:33 +08:00
end
def visibility_limited_to_course_sections? ( user , visibilities = section_visibilities_for ( user ) )
2013-05-03 23:38:33 +08:00
visibilities . all? { | s | s [ :limit_privileges_to_course_section ] }
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2011-06-30 04:05:59 +08:00
# returns a scope, not an array of users/enrollments
2016-02-19 02:09:18 +08:00
def students_visible_to ( user , include : nil )
scope = case include
when :priors
self . all_students
when :inactive
self . admin_visible_students
else
self . students
end
self . apply_enrollment_visibility ( scope , user , nil , include : include )
2011-06-30 04:05:59 +08:00
end
2012-12-12 21:50:15 +08:00
2015-10-24 02:52:51 +08:00
# can apply to user scopes as well if through enrollments (e.g. students, teachers)
2016-02-19 02:09:18 +08:00
def apply_enrollment_visibility ( scope , user , section_ids = nil , include : nil )
2015-10-24 02:52:51 +08:00
if section_ids
scope = scope . where ( 'enrollments.course_section_id' = > section_ids . to_a )
end
visibilities = section_visibilities_for ( user )
2013-06-29 01:18:55 +08:00
visibility_level = enrollment_visibility_level_for ( user , visibilities )
account_admin = visibility_level == :full && visibilities . empty?
# teachers, account admins, and student view students can see student view students
if ! visibilities . any? { | v | v [ :admin ] || v [ :type ] == 'StudentViewEnrollment' } && ! account_admin
2013-03-19 03:07:47 +08:00
scope = scope . where ( " enrollments.type<>'StudentViewEnrollment' " )
2012-03-14 04:08:19 +08:00
end
2016-02-19 02:09:18 +08:00
if include == :inactive && ! [ :full , :sections ] . include? ( visibility_level )
scope = scope . where ( " enrollments.workflow_state <> 'inactive' " ) # don't really include inactive unless user is able to view them
end
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
# See also MessageableUser::Calculator (same logic used to get users across multiple courses) (should refactor)
2013-06-29 01:18:55 +08:00
case visibility_level
2013-05-23 05:13:25 +08:00
when :full , :limited then scope
2013-03-19 03:07:47 +08:00
when :sections then scope . where ( " enrollments.course_section_id IN (?) OR (enrollments.limit_privileges_to_course_section=? AND enrollments.type IN ('TeacherEnrollment', 'TaEnrollment', 'DesignerEnrollment')) " , visibilities . map { | s | s [ :course_section_id ] } , false )
when :restricted then scope . where ( :enrollments = > { :user_id = > visibilities . map { | s | s [ :associated_user_id ] } . compact + [ user ] } )
2013-10-26 05:53:23 +08:00
else scope . none
2012-04-16 23:27:28 +08:00
end
end
2014-11-06 01:22:32 +08:00
def users_visible_to ( user , include_priors = false , opts = { } )
2012-04-16 23:27:28 +08:00
visibilities = section_visibilities_for ( user )
2015-11-21 00:44:25 +08:00
visibility = enrollment_visibility_level_for ( user , visibilities )
scope = if include_priors
users
elsif opts [ :include_inactive ] && [ :full , :sections ] . include? ( visibility )
all_current_users
else
current_users
end
2014-11-06 01:22:32 +08:00
scope = scope . where ( :enrollments = > { :workflow_state = > opts [ :enrollment_state ] } ) if opts [ :enrollment_state ]
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
# See also MessageableUsers (same logic used to get users across multiple courses) (should refactor)
2015-11-21 00:44:25 +08:00
case visibility
2012-04-16 23:27:28 +08:00
when :full then scope
2013-03-19 03:07:47 +08:00
when :sections then scope . where ( :enrollments = > { :course_section_id = > visibilities . map { | s | s [ :course_section_id ] } } )
when :restricted then scope . where ( :enrollments = > { :user_id = > ( visibilities . map { | s | s [ :associated_user_id ] } . compact + [ user ] ) } )
2013-05-23 05:13:25 +08:00
when :limited then scope . where ( " enrollments.type IN ('StudentEnrollment', 'TeacherEnrollment', 'TaEnrollment', 'StudentViewEnrollment') " )
2013-10-26 05:53:23 +08:00
else scope . none
2011-08-19 06:03:33 +08:00
end
end
2015-01-09 01:35:09 +08:00
2015-08-19 22:40:12 +08:00
# returns :all, :none, or an array of section ids
2016-02-04 01:50:29 +08:00
def course_section_visibility ( user , opts = { } )
visibilities = section_visibilities_for ( user , opts )
2014-11-21 22:57:39 +08:00
visibility = enrollment_visibility_level_for ( user , visibilities )
2014-11-25 02:28:16 +08:00
if [ :full , :limited , :restricted , :sections ] . include? ( visibility )
2014-11-21 22:57:39 +08:00
if visibility == :sections || visibilities . all? { | v | [ 'StudentEnrollment' , 'StudentViewEnrollment' , 'ObserverEnrollment' ] . include? v [ :type ] }
2015-08-19 22:40:12 +08:00
visibilities . map { | s | s [ :course_section_id ] }
2012-10-18 02:46:22 +08:00
else
2015-08-19 22:40:12 +08:00
:all
2012-10-18 02:46:22 +08:00
end
else
2015-08-19 22:40:12 +08:00
:none
end
end
2016-02-04 01:50:29 +08:00
def sections_visible_to ( user , sections = active_course_sections , opts = { } )
2015-08-19 22:40:12 +08:00
is_scope = sections . respond_to? ( :where )
2016-02-04 01:50:29 +08:00
section_ids = course_section_visibility ( user , opts )
2015-08-19 22:40:12 +08:00
case section_ids
when :all
sections
when :none
2012-10-18 02:46:22 +08:00
# return an empty set, but keep it as a scope for downstream consistency
2014-11-21 22:57:39 +08:00
is_scope ? sections . none : [ ]
2015-08-19 22:40:12 +08:00
when Array
is_scope ? sections . where ( :id = > section_ids ) : sections . select { | section | section_ids . include? ( section . id ) }
2012-10-18 02:46:22 +08:00
end
end
2014-05-03 00:35:29 +08:00
# derived from policy for Group#grants_right?(user, :read)
2012-10-18 02:46:22 +08:00
def groups_visible_to ( user , groups = active_groups )
2014-05-03 00:35:29 +08:00
if grants_any_right? ( user , :manage_groups , :view_group_pages )
2012-10-18 02:46:22 +08:00
# course-wide permissions; all groups are visible
groups
else
# no course-wide permissions; only groups the user is a member of are
# visible
2013-03-19 03:07:47 +08:00
groups . joins ( :participating_group_memberships ) .
where ( 'group_memberships.user_id' = > user )
2011-09-30 05:36:13 +08:00
end
end
2013-01-08 01:34:01 +08:00
def enrollment_visibility_level_for ( user , visibilities = section_visibilities_for ( user ) , require_message_permission = false )
permissions = require_message_permission ?
[ :send_messages ] :
2015-09-04 23:36:17 +08:00
[ :manage_grades , :manage_students , :manage_admin_users , :read_roster , :view_all_grades , :read_as_admin ]
2014-05-03 00:35:29 +08:00
granted_permissions = self . granted_rights ( user , * permissions )
2013-05-23 05:13:25 +08:00
if granted_permissions . empty?
2013-01-08 01:34:01 +08:00
:restricted # e.g. observer, can only see admins in the course
elsif visibilities . present? && visibility_limited_to_course_sections? ( user , visibilities )
2011-08-19 06:03:33 +08:00
:sections
2013-05-23 05:13:25 +08:00
elsif granted_permissions . eql? [ :read_roster ]
:limited
2011-02-01 09:57:29 +08:00
else
2011-08-19 06:03:33 +08:00
:full
2011-02-01 09:57:29 +08:00
end
end
2012-01-04 04:30:49 +08:00
2013-10-03 04:12:50 +08:00
def invited_count_visible_to ( user )
scope = users_visible_to ( user ) .
2015-05-07 22:24:58 +08:00
where ( " enrollments.workflow_state in ('invited', 'creation_pending') AND enrollments.type != 'StudentViewEnrollment' " )
2014-07-24 01:14:22 +08:00
scope . select ( 'users.id' ) . uniq . count
2013-10-03 04:12:50 +08:00
end
2015-06-17 09:17:28 +08:00
def published?
self . available? || self . completed?
end
2011-02-01 09:57:29 +08:00
def unpublished?
self . created? || self . claimed?
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def tab_configuration
super . map { | h | h . with_indifferent_access } rescue [ ]
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
TAB_HOME = 0
TAB_SYLLABUS = 1
TAB_PAGES = 2
TAB_ASSIGNMENTS = 3
TAB_QUIZZES = 4
TAB_GRADES = 5
TAB_PEOPLE = 6
TAB_GROUPS = 7
TAB_DISCUSSIONS = 8
TAB_MODULES = 10
TAB_FILES = 11
TAB_CONFERENCES = 12
TAB_SETTINGS = 13
TAB_ANNOUNCEMENTS = 14
TAB_OUTCOMES = 15
TAB_COLLABORATIONS = 16
2011-06-22 00:23:08 +08:00
2011-02-01 09:57:29 +08:00
def self . default_tabs
2015-05-20 01:13:32 +08:00
[ {
:id = > TAB_HOME ,
:label = > t ( '#tabs.home' , " Home " ) ,
:css_class = > 'home' ,
:href = > :course_path
} , {
:id = > TAB_ANNOUNCEMENTS ,
:label = > t ( '#tabs.announcements' , " Announcements " ) ,
:css_class = > 'announcements' ,
:href = > :course_announcements_path ,
2015-04-29 04:29:22 +08:00
:screenreader = > t ( " Course Announcements " ) ,
2015-05-20 01:13:32 +08:00
:icon = > 'icon-announcement'
} , {
:id = > TAB_ASSIGNMENTS ,
:label = > t ( '#tabs.assignments' , " Assignments " ) ,
:css_class = > 'assignments' ,
:href = > :course_assignments_path ,
:screenreader = > t ( '#tabs.course_assignments' , " Course Assignments " ) ,
:icon = > 'icon-assignment'
} , {
:id = > TAB_DISCUSSIONS ,
:label = > t ( '#tabs.discussions' , " Discussions " ) ,
:css_class = > 'discussions' ,
:href = > :course_discussion_topics_path ,
2015-04-29 04:29:22 +08:00
:screenreader = > t ( " Course Discussions " ) ,
2015-05-20 01:13:32 +08:00
:icon = > 'icon-discussion'
} , {
:id = > TAB_GRADES ,
2015-07-30 05:50:08 +08:00
:label = > t ( '#tabs.grades' , " Grades " ) ,
2015-05-20 01:13:32 +08:00
:css_class = > 'grades' ,
:href = > :course_grades_path ,
2015-07-30 05:50:08 +08:00
:screenreader = > t ( '#tabs.course_grades' , " Course Grades " )
2015-05-20 01:13:32 +08:00
} , {
:id = > TAB_PEOPLE ,
:label = > t ( '#tabs.people' , " People " ) ,
:css_class = > 'people' ,
:href = > :course_users_path
} , {
:id = > TAB_PAGES ,
:label = > t ( '#tabs.pages' , " Pages " ) ,
:css_class = > 'pages' ,
:href = > :course_wiki_path
} , {
:id = > TAB_FILES ,
:label = > t ( '#tabs.files' , " Files " ) ,
:css_class = > 'files' ,
:href = > :course_files_path ,
2015-04-29 04:29:22 +08:00
:screenreader = > t ( " Course Files " ) ,
2015-06-23 23:20:46 +08:00
:icon = > 'icon-folder'
2015-05-20 01:13:32 +08:00
} , {
:id = > TAB_SYLLABUS ,
:label = > t ( '#tabs.syllabus' , " Syllabus " ) ,
:css_class = > 'syllabus' ,
:href = > :syllabus_course_assignments_path
} , {
:id = > TAB_OUTCOMES ,
:label = > t ( '#tabs.outcomes' , " Outcomes " ) ,
:css_class = > 'outcomes' ,
:href = > :course_outcomes_path
} , {
:id = > TAB_QUIZZES ,
:label = > t ( '#tabs.quizzes' , " Quizzes " ) ,
:css_class = > 'quizzes' ,
:href = > :course_quizzes_path
} , {
:id = > TAB_MODULES ,
:label = > t ( '#tabs.modules' , " Modules " ) ,
:css_class = > 'modules' ,
:href = > :course_context_modules_path
} , {
:id = > TAB_CONFERENCES ,
:label = > t ( '#tabs.conferences' , " Conferences " ) ,
:css_class = > 'conferences' ,
:href = > :course_conferences_path
} , {
:id = > TAB_COLLABORATIONS ,
:label = > t ( '#tabs.collaborations' , " Collaborations " ) ,
:css_class = > 'collaborations' ,
:href = > :course_collaborations_path
} , {
:id = > TAB_SETTINGS ,
:label = > t ( '#tabs.settings' , " Settings " ) ,
:css_class = > 'settings' ,
:href = > :course_settings_path ,
:screenreader = > t ( '#tabs.course_settings' , " Course Settings " )
}
2011-02-01 09:57:29 +08:00
]
end
2012-01-04 04:30:49 +08:00
2012-12-20 07:01:13 +08:00
def tab_hidden? ( id )
tab = self . tab_configuration . find { | t | t [ :id ] == id }
return tab && tab [ :hidden ]
end
basic lti navigation links
By properly configuring external tools (see
/spec/models/course_spec/rb:898 for examples) they can
be added as left-side navigation links to a course,
an account, or to the user profile section of Canvas.
testing notes:
- you have to manually set options on the external tool:
- for user navigation the tool needs to be created on the root account
with the following settings:
{:user_navigation => {:url => <url>, :text => <tab label>} }
(there are also some optional language options you can set using
the :labels attribute)
- for account navigation it's the same
- for course navigation it's the same, except with :course_navigation
there's also some additional options:
:visibility => <value> // public, members, admins
:default => <value> // disabled, enabled
test plan:
- configure a user navigation tool at the root account level,
make sure it shows up in the user's profile section
- configure a course navigation tool at the account level,
make sure it shows up in the course's navigation
- configure a course navigation tool at the course level,
make sure it shows up in the course's navigation
- make sure :default => 'disabled' course navigation tools don't
appear by default in the navigation, but can be enabled on
the course settings page
- make sure :visibility => 'members' only shows up for course members
- make sure :visibility => 'admins' only shows up for course admins
- configure an account navigation tool at the account level,
make sure it shows up in the account's navigation, and
any sub-account's navigation
Change-Id: I977da3c6b89a9e32b4cff4c2b6b221f8162782ff
Reviewed-on: https://gerrit.instructure.com/5427
Reviewed-by: Brian Whitmer <brian@instructure.com>
Tested-by: Hudson <hudson@instructure.com>
2011-08-18 13:49:01 +08:00
def external_tool_tabs ( opts )
2012-01-18 13:32:57 +08:00
tools = self . context_external_tools . active . having_setting ( 'course_navigation' )
2014-09-12 03:44:34 +08:00
tools += ContextExternalTool . active . having_setting ( 'course_navigation' ) . where ( context_type : 'Account' , context_id : account_chain ) . to_a
basic lti navigation links
By properly configuring external tools (see
/spec/models/course_spec/rb:898 for examples) they can
be added as left-side navigation links to a course,
an account, or to the user profile section of Canvas.
testing notes:
- you have to manually set options on the external tool:
- for user navigation the tool needs to be created on the root account
with the following settings:
{:user_navigation => {:url => <url>, :text => <tab label>} }
(there are also some optional language options you can set using
the :labels attribute)
- for account navigation it's the same
- for course navigation it's the same, except with :course_navigation
there's also some additional options:
:visibility => <value> // public, members, admins
:default => <value> // disabled, enabled
test plan:
- configure a user navigation tool at the root account level,
make sure it shows up in the user's profile section
- configure a course navigation tool at the account level,
make sure it shows up in the course's navigation
- configure a course navigation tool at the course level,
make sure it shows up in the course's navigation
- make sure :default => 'disabled' course navigation tools don't
appear by default in the navigation, but can be enabled on
the course settings page
- make sure :visibility => 'members' only shows up for course members
- make sure :visibility => 'admins' only shows up for course admins
- configure an account navigation tool at the account level,
make sure it shows up in the account's navigation, and
any sub-account's navigation
Change-Id: I977da3c6b89a9e32b4cff4c2b6b221f8162782ff
Reviewed-on: https://gerrit.instructure.com/5427
Reviewed-by: Brian Whitmer <brian@instructure.com>
Tested-by: Hudson <hudson@instructure.com>
2011-08-18 13:49:01 +08:00
tools . sort_by ( & :id ) . map do | tool |
{
:id = > tool . asset_string ,
2015-06-16 03:58:15 +08:00
:label = > tool . label_for ( :course_navigation , opts [ :language ] || I18n . locale ) ,
basic lti navigation links
By properly configuring external tools (see
/spec/models/course_spec/rb:898 for examples) they can
be added as left-side navigation links to a course,
an account, or to the user profile section of Canvas.
testing notes:
- you have to manually set options on the external tool:
- for user navigation the tool needs to be created on the root account
with the following settings:
{:user_navigation => {:url => <url>, :text => <tab label>} }
(there are also some optional language options you can set using
the :labels attribute)
- for account navigation it's the same
- for course navigation it's the same, except with :course_navigation
there's also some additional options:
:visibility => <value> // public, members, admins
:default => <value> // disabled, enabled
test plan:
- configure a user navigation tool at the root account level,
make sure it shows up in the user's profile section
- configure a course navigation tool at the account level,
make sure it shows up in the course's navigation
- configure a course navigation tool at the course level,
make sure it shows up in the course's navigation
- make sure :default => 'disabled' course navigation tools don't
appear by default in the navigation, but can be enabled on
the course settings page
- make sure :visibility => 'members' only shows up for course members
- make sure :visibility => 'admins' only shows up for course admins
- configure an account navigation tool at the account level,
make sure it shows up in the account's navigation, and
any sub-account's navigation
Change-Id: I977da3c6b89a9e32b4cff4c2b6b221f8162782ff
Reviewed-on: https://gerrit.instructure.com/5427
Reviewed-by: Brian Whitmer <brian@instructure.com>
Tested-by: Hudson <hudson@instructure.com>
2011-08-18 13:49:01 +08:00
:css_class = > tool . asset_string ,
:href = > :course_external_tool_path ,
2013-04-10 00:48:51 +08:00
:visibility = > tool . course_navigation ( :visibility ) ,
basic lti navigation links
By properly configuring external tools (see
/spec/models/course_spec/rb:898 for examples) they can
be added as left-side navigation links to a course,
an account, or to the user profile section of Canvas.
testing notes:
- you have to manually set options on the external tool:
- for user navigation the tool needs to be created on the root account
with the following settings:
{:user_navigation => {:url => <url>, :text => <tab label>} }
(there are also some optional language options you can set using
the :labels attribute)
- for account navigation it's the same
- for course navigation it's the same, except with :course_navigation
there's also some additional options:
:visibility => <value> // public, members, admins
:default => <value> // disabled, enabled
test plan:
- configure a user navigation tool at the root account level,
make sure it shows up in the user's profile section
- configure a course navigation tool at the account level,
make sure it shows up in the course's navigation
- configure a course navigation tool at the course level,
make sure it shows up in the course's navigation
- make sure :default => 'disabled' course navigation tools don't
appear by default in the navigation, but can be enabled on
the course settings page
- make sure :visibility => 'members' only shows up for course members
- make sure :visibility => 'admins' only shows up for course admins
- configure an account navigation tool at the account level,
make sure it shows up in the account's navigation, and
any sub-account's navigation
Change-Id: I977da3c6b89a9e32b4cff4c2b6b221f8162782ff
Reviewed-on: https://gerrit.instructure.com/5427
Reviewed-by: Brian Whitmer <brian@instructure.com>
Tested-by: Hudson <hudson@instructure.com>
2011-08-18 13:49:01 +08:00
:external = > true ,
2013-04-10 00:48:51 +08:00
:hidden = > tool . course_navigation ( :default ) == 'disabled' ,
basic lti navigation links
By properly configuring external tools (see
/spec/models/course_spec/rb:898 for examples) they can
be added as left-side navigation links to a course,
an account, or to the user profile section of Canvas.
testing notes:
- you have to manually set options on the external tool:
- for user navigation the tool needs to be created on the root account
with the following settings:
{:user_navigation => {:url => <url>, :text => <tab label>} }
(there are also some optional language options you can set using
the :labels attribute)
- for account navigation it's the same
- for course navigation it's the same, except with :course_navigation
there's also some additional options:
:visibility => <value> // public, members, admins
:default => <value> // disabled, enabled
test plan:
- configure a user navigation tool at the root account level,
make sure it shows up in the user's profile section
- configure a course navigation tool at the account level,
make sure it shows up in the course's navigation
- configure a course navigation tool at the course level,
make sure it shows up in the course's navigation
- make sure :default => 'disabled' course navigation tools don't
appear by default in the navigation, but can be enabled on
the course settings page
- make sure :visibility => 'members' only shows up for course members
- make sure :visibility => 'admins' only shows up for course admins
- configure an account navigation tool at the account level,
make sure it shows up in the account's navigation, and
any sub-account's navigation
Change-Id: I977da3c6b89a9e32b4cff4c2b6b221f8162782ff
Reviewed-on: https://gerrit.instructure.com/5427
Reviewed-by: Brian Whitmer <brian@instructure.com>
Tested-by: Hudson <hudson@instructure.com>
2011-08-18 13:49:01 +08:00
:args = > [ self . id , tool . id ]
}
end
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def tabs_available ( user = nil , opts = { } )
2013-12-04 07:00:14 +08:00
opts . reverse_merge! ( :include_external = > true )
cache_key = [ user , opts ] . cache_key
@tabs_available || = { }
@tabs_available [ cache_key ] || = uncached_tabs_available ( user , opts )
end
def uncached_tabs_available ( user , opts )
2012-08-22 02:07:38 +08:00
# make sure t() is called before we switch to the slave, in case we update the user's selected locale in the process
default_tabs = Course . default_tabs
2013-04-03 01:53:08 +08:00
Shackles . activate ( :slave ) do
2012-08-21 01:20:36 +08:00
# We will by default show everything in default_tabs, unless the teacher has configured otherwise.
tabs = self . tab_configuration . compact
settings_tab = default_tabs [ - 1 ]
2014-10-02 00:38:48 +08:00
external_tabs = if opts [ :include_external ]
external_tool_tabs ( opts ) + Lti :: MessageHandler . lti_apps_tabs ( self , [ Lti :: ResourcePlacement :: COURSE_NAVIGATION ] , opts )
else
[ ]
end
2012-08-21 01:20:36 +08:00
tabs = tabs . map do | tab |
default_tab = default_tabs . find { | t | t [ :id ] == tab [ :id ] } || external_tabs . find { | t | t [ :id ] == tab [ :id ] }
if default_tab
tab [ :label ] = default_tab [ :label ]
tab [ :href ] = default_tab [ :href ]
tab [ :css_class ] = default_tab [ :css_class ]
tab [ :args ] = default_tab [ :args ]
tab [ :visibility ] = default_tab [ :visibility ]
tab [ :external ] = default_tab [ :external ]
2015-04-29 04:29:22 +08:00
tab [ :icon ] = default_tab [ :icon ]
tab [ :screenreader ] = default_tab [ :screenreader ]
2012-08-21 01:20:36 +08:00
default_tabs . delete_if { | t | t [ :id ] == tab [ :id ] }
external_tabs . delete_if { | t | t [ :id ] == tab [ :id ] }
tab
else
# Remove any tabs we don't know about in default_tabs (in case we removed them or something, like Groups)
nil
end
2011-08-16 06:34:57 +08:00
end
2012-08-21 01:20:36 +08:00
tabs . compact!
tabs += default_tabs
tabs += external_tabs
# Ensure that Settings is always at the bottom
tabs . delete_if { | t | t [ :id ] == TAB_SETTINGS }
tabs << settings_tab
tabs . each do | tab |
tab [ :hidden_unused ] = true if tab [ :id ] == TAB_MODULES && ! active_record_types [ :modules ]
tab [ :hidden_unused ] = true if tab [ :id ] == TAB_FILES && ! active_record_types [ :files ]
tab [ :hidden_unused ] = true if tab [ :id ] == TAB_QUIZZES && ! active_record_types [ :quizzes ]
tab [ :hidden_unused ] = true if tab [ :id ] == TAB_ASSIGNMENTS && ! active_record_types [ :assignments ]
tab [ :hidden_unused ] = true if tab [ :id ] == TAB_PAGES && ! active_record_types [ :pages ] && ! allow_student_wiki_edits
2014-05-03 00:35:29 +08:00
tab [ :hidden_unused ] = true if tab [ :id ] == TAB_CONFERENCES && ! active_record_types [ :conferences ] && ! self . grants_right? ( user , :create_conferences )
2012-08-21 01:20:36 +08:00
tab [ :hidden_unused ] = true if tab [ :id ] == TAB_ANNOUNCEMENTS && ! active_record_types [ :announcements ]
tab [ :hidden_unused ] = true if tab [ :id ] == TAB_OUTCOMES && ! active_record_types [ :outcomes ]
2015-02-07 12:14:16 +08:00
tab [ :hidden_unused ] = true if tab [ :id ] == TAB_DISCUSSIONS && ! active_record_types [ :discussions ] && ! allow_student_discussion_topics
2011-09-08 02:41:06 +08:00
end
2012-08-21 01:20:36 +08:00
# remove tabs that the user doesn't have access to
unless opts [ :for_reordering ]
2014-05-03 00:35:29 +08:00
unless self . grants_any_right? ( user , opts [ :session ] , :read , :manage_content )
2012-08-21 01:20:36 +08:00
tabs . delete_if { | t | t [ :id ] == TAB_HOME }
tabs . delete_if { | t | t [ :id ] == TAB_ANNOUNCEMENTS }
tabs . delete_if { | t | t [ :id ] == TAB_PAGES }
tabs . delete_if { | t | t [ :id ] == TAB_OUTCOMES }
tabs . delete_if { | t | t [ :id ] == TAB_CONFERENCES }
tabs . delete_if { | t | t [ :id ] == TAB_COLLABORATIONS }
tabs . delete_if { | t | t [ :id ] == TAB_MODULES }
end
2015-03-07 04:10:29 +08:00
unless self . grants_any_right? ( user , opts [ :session ] , :participate_as_student , :read_as_admin )
2012-08-21 01:20:36 +08:00
tabs . delete_if { | t | t [ :visibility ] == 'members' }
end
2014-05-03 00:35:29 +08:00
unless self . grants_any_right? ( user , opts [ :session ] , :read , :manage_content , :manage_assignments )
2012-08-21 01:20:36 +08:00
tabs . delete_if { | t | t [ :id ] == TAB_ASSIGNMENTS }
tabs . delete_if { | t | t [ :id ] == TAB_QUIZZES }
2011-09-08 02:41:06 +08:00
end
2014-05-03 00:35:29 +08:00
unless self . grants_any_right? ( user , opts [ :session ] , :read , :read_syllabus , :manage_content , :manage_assignments )
2013-04-06 04:40:05 +08:00
tabs . delete_if { | t | t [ :id ] == TAB_SYLLABUS }
end
2015-03-07 04:10:29 +08:00
tabs . delete_if { | t | t [ :visibility ] == 'admins' } unless self . grants_right? ( user , opts [ :session ] , :read_as_admin )
2014-05-03 00:35:29 +08:00
if self . grants_any_right? ( user , opts [ :session ] , :manage_content , :manage_assignments )
2012-08-21 01:20:36 +08:00
tabs . detect { | t | t [ :id ] == TAB_ASSIGNMENTS } [ :manageable ] = true
tabs . detect { | t | t [ :id ] == TAB_SYLLABUS } [ :manageable ] = true
tabs . detect { | t | t [ :id ] == TAB_QUIZZES } [ :manageable ] = true
end
2015-03-07 04:10:29 +08:00
tabs . delete_if { | t | t [ :hidden ] && t [ :external ] } unless opts [ :api ] && self . grants_right? ( user , :read_as_admin )
2014-05-03 00:35:29 +08:00
tabs . delete_if { | t | t [ :id ] == TAB_GRADES } unless self . grants_any_right? ( user , opts [ :session ] , :read_grades , :view_all_grades , :manage_grades )
tabs . detect { | t | t [ :id ] == TAB_GRADES } [ :manageable ] = true if self . grants_any_right? ( user , opts [ :session ] , :view_all_grades , :manage_grades )
tabs . delete_if { | t | t [ :id ] == TAB_PEOPLE } unless self . grants_any_right? ( user , opts [ :session ] , :read_roster , :manage_students , :manage_admin_users )
tabs . detect { | t | t [ :id ] == TAB_PEOPLE } [ :manageable ] = true if self . grants_any_right? ( user , opts [ :session ] , :manage_students , :manage_admin_users )
tabs . delete_if { | t | t [ :id ] == TAB_FILES } unless self . grants_any_right? ( user , opts [ :session ] , :read , :manage_files )
2015-04-03 21:19:43 +08:00
tabs . detect { | t | t [ :id ] == TAB_FILES } [ :manageable ] = true if self . grants_right? ( user , opts [ :session ] , :manage_files )
2014-05-03 00:35:29 +08:00
tabs . delete_if { | t | t [ :id ] == TAB_DISCUSSIONS } unless self . grants_any_right? ( user , opts [ :session ] , :read_forum , :moderate_forum , :post_to_forum )
2012-08-21 01:20:36 +08:00
tabs . detect { | t | t [ :id ] == TAB_DISCUSSIONS } [ :manageable ] = true if self . grants_right? ( user , opts [ :session ] , :moderate_forum )
tabs . delete_if { | t | t [ :id ] == TAB_SETTINGS } unless self . grants_right? ( user , opts [ :session ] , :read_as_admin )
2014-05-03 00:35:29 +08:00
if ! user || ! self . grants_right? ( user , :manage_content )
2012-08-21 01:20:36 +08:00
# remove some tabs for logged-out users or non-students
2014-05-03 00:35:29 +08:00
unless grants_any_right? ( user , :read_as_admin , :participate_as_student )
2013-12-18 05:21:40 +08:00
tabs . delete_if { | t | [ TAB_PEOPLE , TAB_OUTCOMES ] . include? ( t [ :id ] ) }
2012-08-21 01:20:36 +08:00
end
2011-08-16 06:34:57 +08:00
2016-02-02 07:59:22 +08:00
unless announcements . temp_record . grants_right? ( user , :read )
2012-10-23 05:06:28 +08:00
tabs . delete_if { | t | t [ :id ] == TAB_ANNOUNCEMENTS }
end
2012-08-21 01:20:36 +08:00
# remove hidden tabs from students
2015-03-07 04:10:29 +08:00
unless self . grants_right? ( user , opts [ :session ] , :read_as_admin )
tabs . delete_if { | t | ( t [ :hidden ] || ( t [ :hidden_unused ] && ! opts [ :include_hidden_unused ] ) ) && ! t [ :manageable ] }
end
2012-08-21 01:20:36 +08:00
end
2011-09-08 02:41:06 +08:00
end
2012-08-21 01:20:36 +08:00
# Uncommenting these lines will always put hidden links after visible links
# tabs.each_with_index{|t, i| t[:sort_index] = i }
# tabs = tabs.sort_by{|t| [t[:hidden_unused] || t[:hidden] ? 1 : 0, t[:sort_index]] } if !self.tab_configuration || self.tab_configuration.empty?
tabs
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 allow_wiki_comments
read_attribute ( :allow_wiki_comments )
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def account_name
self . account . name rescue nil
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def term_name
self . enrollment_term . name rescue nil
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def enable_user_notes
2011-12-28 05:57:56 +08:00
root_account . enable_user_notes rescue false
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2011-02-03 05:30:07 +08:00
def equella_settings
account = self . account
while account
settings = account . equella_settings
return settings if settings
account = account . parent_account
end
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
# This will move the course to be in the specified account.
# All enrollments, sections, and other objects attached to the course will also be updated.
2012-01-04 04:30:49 +08:00
def move_to_account ( new_root_account , new_sub_account = nil )
2011-02-01 09:57:29 +08:00
self . account = new_sub_account || new_root_account
self . save if new_sub_account
self . root_account = new_root_account
user_ids = [ ]
2012-01-04 04:30:49 +08:00
2013-03-19 03:07:47 +08:00
CourseSection . where ( :course_id = > self ) . each do | cs |
2011-02-01 09:57:29 +08:00
cs . update_attribute ( :root_account_id , new_root_account . id )
end
2012-01-04 04:30:49 +08:00
2013-03-19 03:07:47 +08:00
Enrollment . where ( :course_id = > self ) . each do | e |
2011-02-01 09:57:29 +08:00
e . update_attribute ( :root_account_id , new_root_account . id )
user_ids << e . user_id
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
self . save
User . update_account_associations ( user_ids )
end
2011-08-11 01:48:34 +08:00
cattr_accessor :settings_options
self . settings_options = { }
2013-02-08 03:56:05 +08:00
def self . add_setting ( setting , opts = { } )
setting = setting . to_sym
settings_options [ setting ] = opts
cast_expression = " val.to_s "
if opts [ :boolean ]
opts [ :default ] || = false
cast_expression = " Canvas::Plugin.value_to_boolean(val) "
end
2013-02-23 02:41:37 +08:00
class_eval <<-CODE
2013-02-08 03:56:05 +08:00
def #{setting}
2015-03-28 01:16:08 +08:00
if Course . settings_options [ #{setting.inspect}][:inherited]
2015-07-01 23:31:41 +08:00
inherited = RequestCache . cache ( 'inherited_course_setting' , #{setting.inspect}, self.global_account_id) do
self . account . send ( #{setting.inspect})
end
2015-03-28 01:16:08 +08:00
if inherited [ :locked ] || settings_frd [ #{setting.inspect}].nil?
inherited [ :value ]
else
settings_frd [ #{setting.inspect}]
end
elsif settings_frd [ #{setting.inspect}].nil? && !@disable_setting_defaults
2013-02-12 03:52:53 +08:00
default = Course . settings_options [ #{setting.inspect}][:default]
default . respond_to? ( :call ) ? default . call ( self ) : default
else
2013-02-08 03:56:05 +08:00
settings_frd [ #{setting.inspect}]
2013-02-12 03:52:53 +08:00
end
2013-02-08 03:56:05 +08:00
end
def #{setting}=(val)
settings_frd [ #{setting.inspect}] = #{cast_expression}
end
CODE
alias_method " #{ setting } ? " , setting if opts [ :boolean ]
if opts [ :alias ]
alias_method opts [ :alias ] , setting
alias_method " #{ opts [ :alias ] } = " , " #{ setting } = "
alias_method " #{ opts [ :alias ] } ? " , " #{ setting } ? "
2013-01-09 07:02:16 +08:00
end
end
2013-02-08 03:56:05 +08:00
# unfortunately we decided to pluralize this in the API after the fact...
# so now we pluralize it everywhere except the actual settings hash and
# course import/export :(
add_setting :hide_final_grade , :alias = > :hide_final_grades , :boolean = > true
add_setting :hide_distribution_graphs , :boolean = > true
add_setting :allow_student_discussion_topics , :boolean = > true , :default = > true
2013-02-08 04:09:05 +08:00
add_setting :allow_student_discussion_editing , :boolean = > true , :default = > true
allow displaing total grade as points for students
fixes CNVS-9874
when teacher choses to show total grade as points in GB2, a setting is saved
student grade summary page shows the total grade in same format
if assignment groups are weighted, grade is displayed as a percentage again
test plan:
- new addition:
- go to a course where GB2 is displaying totals as points BUT has no DB setting about show_point_totals
(ask mike if you need help getting to this state)
- as a student look at the grade summary page, total should be a percent
- as a teacher in that class, go to GB2 and wait for 5 seconds
- as that same student, go back to grade summary, total should be points now
- as a teacher, change total grade to show as points in GB2
- when you return to the GB, even from a new browser, this should be the same
- in GB, tooltip should show points if cell shows percent, and visa vera
- as a student in that class, you should see your total grade as points
- grade tooltip should show points if cell shows percent, and visa vera
- switch to weighted assignment groups
- total grade as a teacher and student should switch back to percent
- switch back to non-weighted
- total grade should stay a percent until explicitly changed back to points
- GB2 weighting and points/percent switching should have no unintended effects on the GB
- student grade summary page should display everything in a consistent manner
Change-Id: Id0c4f496576c226eb7000d6684a37faf0b439359
Reviewed-on: https://gerrit.instructure.com/28780
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Caleb Guanzon <cguanzon@instructure.com>
Reviewed-by: Simon Williams <simon@instructure.com>
Product-Review: Mike Nomitch <mnomitch@instructure.com>
2014-01-15 01:06:03 +08:00
add_setting :show_total_grade_as_points , :boolean = > true , :default = > false
2013-02-05 05:53:31 +08:00
add_setting :lock_all_announcements , :boolean = > true , :default = > false
2013-02-12 03:52:53 +08:00
add_setting :large_roster , :boolean = > true , :default = > lambda { | c | c . root_account . large_course_rosters? }
2013-04-06 04:40:05 +08:00
add_setting :public_syllabus , :boolean = > true , :default = > false
2014-09-30 21:13:32 +08:00
add_setting :course_format
2015-07-21 05:19:44 +08:00
add_setting :organize_epub_by_content_type , :boolean = > true , :default = > false
2015-01-09 01:35:09 +08:00
add_setting :is_public_to_auth_users , :boolean = > true , :default = > false
2013-02-08 04:09:05 +08:00
2015-03-28 01:16:08 +08:00
add_setting :restrict_student_future_view , :boolean = > true , :inherited = > true
add_setting :restrict_student_past_view , :boolean = > true , :inherited = > true
2015-03-07 07:17:57 +08:00
2013-02-08 04:09:05 +08:00
def user_can_manage_own_discussion_posts? ( user )
return true if allow_student_discussion_editing?
return true if user_is_instructor? ( user )
false
end
2013-01-09 07:02:16 +08:00
2012-09-06 05:57:33 +08:00
def filter_attributes_for_user ( hash , user , session )
2015-08-18 05:17:50 +08:00
hash . delete ( 'hide_final_grades' ) if hash . key? ( 'hide_final_grades' ) && ! grants_right? ( user , :update )
2012-09-06 05:57:33 +08:00
hash
end
2013-02-08 03:56:05 +08:00
# DEPRECATED, use setting accessors instead
2011-08-11 01:48:34 +08:00
def settings = ( hash )
2013-02-08 03:56:05 +08:00
write_attribute ( :settings , hash )
2011-08-11 01:48:34 +08:00
end
2013-02-08 03:56:05 +08:00
# frozen, because you should use setters
2011-08-11 01:48:34 +08:00
def settings
2013-02-08 03:56:05 +08:00
settings_frd . dup . freeze
end
def settings_frd
2015-12-29 03:01:30 +08:00
read_or_initialize_attribute ( :settings , { } )
2013-02-08 03:56:05 +08:00
end
def disable_setting_defaults
@disable_setting_defaults = true
yield
ensure
@disable_setting_defaults = nil
2011-08-11 01:48:34 +08:00
end
2011-08-23 06:25:28 +08:00
def reset_content
Course . transaction do
2011-08-31 00:14:27 +08:00
new_course = Course . new
2014-12-27 02:05:00 +08:00
self . attributes . delete_if { | k , v | [ :id , :created_at , :updated_at , :syllabus_body , :wiki_id , :default_view , :tab_configuration , :lti_context_id , :workflow_state ] . include? ( k . to_sym ) } . each do | key , val |
2011-10-13 07:28:30 +08:00
new_course . write_attribute ( key , val )
2011-08-23 06:25:28 +08:00
end
2014-12-27 02:05:00 +08:00
new_course . workflow_state = 'created'
2012-06-26 02:18:45 +08:00
# there's a unique constraint on this, so we need to clear it out
self . self_enrollment_code = nil
self . self_enrollment = false
2011-08-31 00:14:27 +08:00
# The order here is important; we have to set our sis id to nil and save first
# so that the new course can be saved, then we need the new course saved to
# get its id to move over sections and enrollments. Setting this course to
# deleted has to be last otherwise it would set all the enrollments to
# deleted before they got moved
2015-08-17 21:49:06 +08:00
self . uuid = self . sis_source_id = self . sis_batch_id = self . integration_id = nil ;
2011-08-25 00:25:11 +08:00
self . save!
2011-10-13 07:28:30 +08:00
Course . process_as_sis { new_course . save! }
2013-03-19 03:07:47 +08:00
self . course_sections . update_all ( :course_id = > new_course )
2011-08-31 00:14:27 +08:00
# we also want to bring along prior enrollments, so don't use the enrollments
# association
2015-06-13 04:34:40 +08:00
Enrollment . where ( :course_id = > self ) . update_all ( :course_id = > new_course , :updated_at = > Time . now . utc )
2015-10-27 04:25:42 +08:00
User . where ( id : new_course . all_enrollments . select ( :user_id ) ) .
2015-06-13 04:34:40 +08:00
update_all ( updated_at : Time . now . utc )
2011-08-31 00:14:27 +08:00
self . replacement_course_id = new_course . id
self . workflow_state = 'deleted'
self . save!
2013-08-06 05:06:27 +08:00
# Assign original course profile to the new course (automatically saves it)
2014-12-04 05:49:51 +08:00
new_course . profile = self . profile unless self . profile . new_record?
2015-01-09 01:35:09 +08:00
2011-10-13 07:28:30 +08:00
Course . find ( new_course . id )
2011-08-23 06:25:28 +08:00
end
end
2011-11-15 03:39:28 +08:00
introduce preferred user list search mode
closes #7059, #7411
preferred is a compromise between open registration and closed
registration. if a single user is found, that user is found without
introducing any temporary user to allow user disambiguation. if no
users are found, or multiple users are found, it's the same behavior
as open registration.
as part of this, the "only search existing users" checkbox has been
removed. instead, preferred searching will be used in the following
cases:
* adding an admin, and open registration is enabled
* adding an admin, open registration is disabled, and the user
has permission to create new users
* adding a user to a course, open registration is enabled, and
delegated authentication is in use
* adding a user to a course, open registration is disabled, and
the user has permission to create new users
the notable case that preferred searching is not used, and the
user would want to use search existing users is open registration
enabled, and adding a user to a course. in this case, a temporary
user will be created instead of sending the invitation to the
existing user. however, the end user experience is identical
(invitation sent to e-mail, invitation visible in the same locations
in the UI), with the small exception that the end user gets the
opportunity to create a separate account if desired (but not
preferred).
test plan:
* go through each of the cases above for a user that doesn't exist
yet, a single matching user exists, or multiple users exist.
it should behave as described above
Change-Id: Idbc7fe23bfc7430b4cd25f7981f5dc08b9e4ffda
Reviewed-on: https://gerrit.instructure.com/8966
Tested-by: Hudson <hudson@instructure.com>
Reviewed-by: Brian Palmer <brianp@instructure.com>
2012-02-24 06:24:48 +08:00
def user_list_search_mode_for ( user )
if self . root_account . open_registration?
return self . root_account . delegated_authentication? ? :preferred : :open
end
return :preferred if self . root_account . grants_right? ( user , :manage_user_logins )
:closed
end
2012-03-03 08:05:06 +08:00
def participating_users ( user_ids )
2015-07-17 05:53:07 +08:00
enrollments = self . enrollments . eager_load ( :user ) .
2013-03-19 03:07:47 +08:00
where ( :enrollments = > { :workflow_state = > 'active' } , :users = > { :id = > user_ids } )
2015-08-18 03:04:27 +08:00
Canvas :: Builders :: EnrollmentDateBuilder . preload ( enrollments )
2012-03-03 08:05:06 +08:00
enrollments . select { | e | e . active? } . map ( & :user ) . uniq
end
2012-12-12 21:50:15 +08:00
2012-03-14 04:08:19 +08:00
def student_view_student
fake_student = find_or_create_student_view_student
fake_student = sync_enrollments ( fake_student )
fake_student
end
# part of the way we isolate this fake student from places we don't want it
# to appear is to ensure that it does not have a pseudonym or any
# account_associations. if either of these conditions is false, something is
# wrong.
def find_or_create_student_view_student
if self . student_view_students . active . count == 0
fake_student = nil
User . skip_updating_account_associations do
fake_student = User . new ( :name = > t ( 'student_view_student_name' , " Test Student " ) )
fake_student . preferences [ :fake_student ] = true
fake_student . workflow_state = 'registered'
fake_student . save
2012-03-31 04:38:56 +08:00
# hash the unique_id so that it's hard to accidently enroll the user in
# a course by entering something in a user list. :(
fake_student . pseudonyms . create! ( :account = > self . root_account ,
:unique_id = > Canvas :: Security . hmac_sha1 ( " Test Student_ #{ fake_student . id } " ) )
2012-03-14 04:08:19 +08:00
end
fake_student
else
self . student_view_students . active . first
end
end
private :find_or_create_student_view_student
2012-12-12 21:50:15 +08:00
2012-03-14 04:08:19 +08:00
# we want to make sure the student view student is always enrolled in all the
# sections of the course, so that a section limited teacher can grade them.
def sync_enrollments ( fake_student )
2013-12-06 06:08:10 +08:00
self . default_section unless course_sections . active . any?
2014-01-18 02:50:57 +08:00
Enrollment . suspend_callbacks ( :update_cached_due_dates ) do
2013-10-10 06:21:57 +08:00
self . course_sections . active . each do | section |
# enroll fake_student will only create the enrollment if it doesn't already exist
self . enroll_user ( fake_student , 'StudentViewEnrollment' ,
:allow_multiple_enrollments = > true ,
:section = > section ,
:enrollment_state = > 'active' ,
:no_notify = > true ,
:skip_touch_user = > true )
end
2012-03-14 04:08:19 +08:00
end
2013-10-10 06:21:57 +08:00
DueDateCacher . recompute_course ( self )
2012-03-14 04:08:19 +08:00
fake_student
end
private :sync_enrollments
2012-11-17 06:00:05 +08:00
def associated_shards
[ Shard . default ]
end
2012-12-12 21:50:15 +08:00
def includes_student? ( user )
2015-02-21 04:44:04 +08:00
includes_user? ( user , student_enrollments )
end
def includes_user? ( user , enrollment_scope = enrollments )
2014-09-12 03:44:34 +08:00
return false if user . nil? || user . new_record?
2015-02-21 04:44:04 +08:00
enrollment_scope . where ( user_id : user ) . exists?
2012-12-12 21:50:15 +08:00
end
2014-02-20 05:07:34 +08:00
def update_one ( update_params , user , update_source = :manual )
options = { source : update_source }
2013-01-16 07:37:54 +08:00
case update_params [ :event ]
when 'offer'
if self . completed?
self . unconclude!
2014-02-20 05:07:34 +08:00
Auditors :: Course . record_unconcluded ( self , user , options )
2013-01-16 07:37:54 +08:00
else
2014-02-20 05:07:34 +08:00
unless self . available?
self . offer!
Auditors :: Course . record_published ( self , user , options )
end
2013-01-16 07:37:54 +08:00
end
when 'conclude'
2014-02-20 05:07:34 +08:00
unless self . completed?
self . complete!
Auditors :: Course . record_concluded ( self , user , options )
end
2013-01-16 07:37:54 +08:00
when 'delete'
self . sis_source_id = nil
self . workflow_state = 'deleted'
self . save!
2014-02-20 05:07:34 +08:00
Auditors :: Course . record_deleted ( self , user , options )
2013-02-26 04:13:57 +08:00
when 'undelete'
self . workflow_state = 'claimed'
self . save!
2014-02-20 05:07:34 +08:00
Auditors :: Course . record_restored ( self , user , options )
2013-01-16 07:37:54 +08:00
end
end
2014-02-20 05:07:34 +08:00
def self . do_batch_update ( progress , user , course_ids , update_params , update_source = :manual )
2013-02-06 07:31:21 +08:00
account = progress . context
progress_runner = ProgressRunner . new ( progress )
progress_runner . completed_message do | completed_count |
t ( 'batch_update_message' , {
2013-01-16 07:37:54 +08:00
:one = > " 1 course processed " ,
:other = > " %{count} courses processed "
} ,
:count = > completed_count )
end
2013-02-06 07:31:21 +08:00
progress_runner . do_batch_update ( course_ids ) do | course_id |
2014-09-12 03:44:34 +08:00
course = account . associated_courses . where ( id : course_id ) . first
2013-02-26 04:13:57 +08:00
raise t ( 'course_not_found' , " The course was not found " ) unless course &&
( course . workflow_state != 'deleted' || update_params [ :event ] == 'undelete' )
2013-02-06 07:31:21 +08:00
raise t ( 'access_denied' , " Access was denied " ) unless course . grants_right? user , :update
2014-02-20 05:07:34 +08:00
course . update_one ( update_params , user , update_source )
2013-02-06 07:31:21 +08:00
end
2013-01-16 07:37:54 +08:00
end
2014-02-20 05:07:34 +08:00
def self . batch_update ( account , user , course_ids , update_params , update_source = :manual )
2013-01-16 07:37:54 +08:00
progress = account . progresses . create! :tag = > " course_batch_update " , :completion = > 0 . 0
2013-06-04 06:35:34 +08:00
job = Course . send_later_enqueue_args ( :do_batch_update ,
{ no_delay : true } ,
2014-02-20 05:07:34 +08:00
progress , user , course_ids , update_params , update_source )
2013-01-16 07:37:54 +08:00
progress . user_id = user . id
progress . delayed_job_id = job . id
progress . save!
progress
end
2013-03-12 05:14:19 +08:00
2015-10-06 05:48:37 +08:00
def re_send_invitations! ( from_user )
2015-10-24 02:52:51 +08:00
self . apply_enrollment_visibility ( self . student_enrollments , from_user ) . invited . except ( :preload ) . preload ( user : :communication_channels ) . find_each do | e |
2013-03-12 05:14:19 +08:00
e . re_send_confirmation! if e . invited?
end
end
2013-07-10 00:14:52 +08:00
2013-09-18 06:24:57 +08:00
def serialize_permissions ( permissions_hash , user , session )
permissions_hash . merge (
2015-03-07 08:33:59 +08:00
create_discussion_topic : DiscussionTopic . context_allows_user_to_create? ( self , user , session ) ,
create_announcement : Announcement . context_allows_user_to_create? ( self , user , session )
2013-09-18 06:24:57 +08:00
)
end
2013-11-20 02:22:22 +08:00
2015-06-26 03:35:23 +08:00
def active_section_count
@section_count || = self . active_course_sections . count
end
2013-11-20 02:22:22 +08:00
def multiple_sections?
2015-06-26 03:35:23 +08:00
active_section_count > 1
2013-11-20 02:22:22 +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 )
if self . grants_right? ( user , :read_as_admin )
self . content_exports . admin ( user )
else
self . content_exports . non_admin ( user )
end
end
2014-09-26 05:38:07 +08:00
2014-11-17 22:52:50 +08:00
%w{ student_count primary_enrollment_type primary_enrollment_role_id primary_enrollment_rank primary_enrollment_state invitation } . each do | method |
2014-09-30 12:04:39 +08:00
class_eval <<-RUBY
def #{method}
read_attribute ( : #{method}) || @#{method}
end
RUBY
2014-09-26 05:38:07 +08:00
end
2015-02-21 03:45:22 +08:00
def touch_content_if_public_visibility_changed ( changes )
if changes [ :is_public ] || changes [ :is_public_to_auth_users ]
2015-12-10 13:08:46 +08:00
self . assignments . touch_all
self . attachments . touch_all
self . calendar_events . touch_all
self . context_modules . touch_all
self . discussion_topics . touch_all
self . quizzes . touch_all
2015-02-21 03:45:22 +08:00
self . wiki . touch
2015-12-10 13:08:46 +08:00
self . wiki . wiki_pages . touch_all
2015-02-21 03:45:22 +08:00
end
end
2015-05-29 07:07:10 +08:00
2015-10-08 00:55:55 +08:00
def touch_admins_later
send_later_enqueue_args ( :touch_admins , { :run_at = > 15 . seconds . from_now , :singleton = > " course_touch_admins_ #{ global_id } " } )
end
def touch_admins
User . where ( id : self . admins ) . touch_all
end
2015-05-29 07:07:10 +08:00
def list_students_by_sortable_name?
feature_enabled? ( :gradebook_list_students_by_sortable_name )
end
2015-06-13 04:58:02 +08:00
##
# Returns a boolean describing if the user passed in has marked this course
# as a favorite.
def favorite_for_user? ( user )
user . favorites . where ( :context_type = > 'Course' , :context_id = > self ) . exists?
end
2015-10-15 21:19:41 +08:00
def nickname_for ( user , fallback = :name )
nickname = user && user . course_nickname ( self )
nickname || = self . send ( fallback ) if fallback
nickname
end
2015-10-21 00:43:35 +08:00
def refresh_content_participation_counts ( _progress )
content_participation_counts . each ( & :refresh_unread_count )
end
2015-10-22 02:45:51 +08:00
def name
return @nickname if @nickname
read_attribute ( :name )
end
def apply_nickname_for! ( user )
@nickname = nickname_for ( user , nil )
end
2011-02-01 09:57:29 +08:00
end