diff --git a/.active_record_doctor.rb b/.active_record_doctor.rb new file mode 100644 index 0000000000..d113267c7e --- /dev/null +++ b/.active_record_doctor.rb @@ -0,0 +1,10 @@ +ActiveRecordDoctor.configure do + # api_key and schema are NOT NULL but are populated by AutotestSetting's + # before_create callback (register_autotester), which runs after validation. + # A presence validator can never pass on create, so these are exempt. + detector :missing_presence_validation, + ignore_attributes: [ + 'AutotestSetting.api_key', + 'AutotestSetting.schema' + ] +end diff --git a/Changelog.md b/Changelog.md index 5a725700d4..26a0f7535d 100644 --- a/Changelog.md +++ b/Changelog.md @@ -30,6 +30,7 @@ - Fixed `(hidden)` assignment labeling for assignments with `visible_on` and/or `visible_until` set (#7944) ### 🔧 Internal changes +- Added `NOT NULL` constraints and presence/inclusion validators flagged by `active_record_doctor` checks `missing_non_null_constraint` and `missing_presence_validation` - Added tests for `MarksGradersController` to achieve full test coverage for `randomly_assign` (#7947) - Refactored `AuthenticationHelper#sign_in` to set session values directly instead of going through `MainController#login` (#7962) - Updated `MainController` specs to dispatch `post :login` directly in tests that assert on login's response, instead of relying on `sign_in`'s internal request (#7962) diff --git a/app/jobs/split_pdf_job.rb b/app/jobs/split_pdf_job.rb index b0771fd060..6cfceea0c8 100644 --- a/app/jobs/split_pdf_job.rb +++ b/app/jobs/split_pdf_job.rb @@ -145,6 +145,7 @@ def perform(exam_template, _path, split_pdf_log, _original_filename = nil, _role m_logger.log(status) split_page_updates << { id: split_page_id, + split_pdf_log_id: split_pdf_log.id, status: status, group_id: nil, exam_page_number: nil @@ -156,6 +157,7 @@ def perform(exam_template, _path, split_pdf_log, _original_filename = nil, _role m_logger.log(status) split_page_updates << { id: split_page_id, + split_pdf_log_id: split_pdf_log.id, status: status, group_id: nil, exam_page_number: nil @@ -166,6 +168,7 @@ def perform(exam_template, _path, split_pdf_log, _original_filename = nil, _role m_logger.log("#{m[:short_id]}: exam number #{m[:exam_num]}, page #{m[:page_num]}") split_page_updates << { id: split_page_id, + split_pdf_log_id: split_pdf_log.id, status: status, group_id: group_id, exam_page_number: m[:page_num].to_i @@ -173,7 +176,7 @@ def perform(exam_template, _path, split_pdf_log, _original_filename = nil, _role end end SplitPage.upsert_all(split_page_updates, returning: false) - num_complete = save_pages(exam_template, partial_exams, on_duplicate) + num_complete = save_pages(exam_template, partial_exams, split_pdf_log, on_duplicate) num_incomplete = partial_exams.length - num_complete split_pdf_log.update( @@ -210,7 +213,7 @@ def perform(exam_template, _path, split_pdf_log, _original_filename = nil, _role end # Save the pages into groups for this assignment - def save_pages(exam_template, partial_exams, on_duplicate = nil) + def save_pages(exam_template, partial_exams, split_pdf_log, on_duplicate = nil) complete_dir = File.join(exam_template.base_path, 'complete') incomplete_dir = File.join(exam_template.base_path, 'incomplete') error_dir = File.join(exam_template.base_path, 'error') @@ -258,7 +261,7 @@ def save_pages(exam_template, partial_exams, on_duplicate = nil) status = "ERROR: #{exam_template.name}: exam number #{exam_num}, page #{page_num} already exists" end # update status of page - split_page_updates << { id: split_page_id, status: status } + split_page_updates << { id: split_page_id, split_pdf_log_id: split_pdf_log.id, status: status } end SplitPage.upsert_all(split_page_updates, returning: false) diff --git a/app/models/annotation.rb b/app/models/annotation.rb index 4ea95528bb..e12bb25c71 100644 --- a/app/models/annotation.rb +++ b/app/models/annotation.rb @@ -40,7 +40,7 @@ class Annotation < ApplicationRecord belongs_to :submission_file belongs_to :annotation_text - belongs_to :creator, polymorphic: true + belongs_to :creator, polymorphic: true, optional: true belongs_to :result has_one :course, through: :submission_file diff --git a/app/models/annotation_text.rb b/app/models/annotation_text.rb index 743bd194f0..01414a8793 100644 --- a/app/models/annotation_text.rb +++ b/app/models/annotation_text.rb @@ -22,7 +22,7 @@ # # rubocop:enable Layout/LineLength, Lint/RedundantCopDisableDirective class AnnotationText < ApplicationRecord - belongs_to :creator, class_name: 'Role' + belongs_to :creator, class_name: 'Role', optional: true belongs_to :last_editor, class_name: 'Role', optional: true has_one :course, through: :creator diff --git a/app/models/assessment.rb b/app/models/assessment.rb index d905b4af76..39af336166 100644 --- a/app/models/assessment.rb +++ b/app/models/assessment.rb @@ -65,6 +65,9 @@ class Assessment < ApplicationRecord validate :visible_dates_are_valid validates :description, presence: true validates :is_hidden, inclusion: { in: [true, false] } + validates :show_total, inclusion: { in: [true, false] } + validates :type, presence: true + validates :message, exclusion: { in: [nil] } validates :short_identifier, format: { with: /\A[a-zA-Z0-9\-_]+\z/ } def self.type diff --git a/app/models/assignment.rb b/app/models/assignment.rb index cb420174e9..46f7d5e4b6 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -419,7 +419,7 @@ def all_grouping_data def add_group(new_group_name = nil) if group_name_autogenerated - group = self.course.groups.new + group = self.course.groups.new(group_name: SecureRandom.uuid) group.save(validate: false) group.group_name = group.get_autogenerated_group_name group.save @@ -447,7 +447,7 @@ def add_group_api(new_group_name = nil, members = []) g.repo_name = student_user_name end elsif group_name_autogenerated - group = course.groups.new + group = course.groups.new(group_name: SecureRandom.uuid) group.save(validate: false) group.group_name = group.get_autogenerated_group_name else diff --git a/app/models/assignment_properties.rb b/app/models/assignment_properties.rb index f1e31dbd97..bd30658f33 100644 --- a/app/models/assignment_properties.rb +++ b/app/models/assignment_properties.rb @@ -110,6 +110,12 @@ class AssignmentProperties < ApplicationRecord end validates :scanned_exam, inclusion: { in: [true, false] } + validates :allow_remarks, inclusion: { in: [true, false] } + validates :anonymize_groups, inclusion: { in: [true, false] } + validates :hide_unassigned_criteria, inclusion: { in: [true, false] } + validates :is_timed, inclusion: { in: [true, false] } + validates :starter_files_after_due, inclusion: { in: [true, false] } + validates :release_with_urls, inclusion: { in: [true, false] } validate :minimum_number_of_groups diff --git a/app/models/course.rb b/app/models/course.rb index a1e89ed7b5..d9d68996a3 100644 --- a/app/models/course.rb +++ b/app/models/course.rb @@ -48,7 +48,7 @@ class Course < ApplicationRecord # Note rails provides built-in sanitization via active record. validates :display_name, presence: true validates :is_hidden, inclusion: { in: [true, false] } - validates :max_file_size, numericality: { greater_than_or_equal_to: 0 } + validates :max_file_size, presence: true, numericality: { greater_than_or_equal_to: 0 } validate :start_at_before_or_equal_to_end_at after_save_commit :update_repo_max_file_size diff --git a/app/models/criterion.rb b/app/models/criterion.rb index 95c3761c59..ec8c4aab6a 100644 --- a/app/models/criterion.rb +++ b/app/models/criterion.rb @@ -50,6 +50,11 @@ class Criterion < ApplicationRecord validates :name, uniqueness: { scope: :assessment_id } validates :bonus, inclusion: { in: [true, false] } + validates :type, presence: true + validates :position, presence: true + validates :ta_visible, inclusion: { in: [true, false] } + validates :peer_visible, inclusion: { in: [true, false] } + validates :description, exclusion: { in: [nil] } validates :max_mark, presence: true validates :max_mark, numericality: { greater_than: 0 } @@ -79,6 +84,7 @@ def update_assigned_groups_count def self.randomly_assign_tas(criterion_ids, ta_ids, assignment) assign_tas(criterion_ids, ta_ids, assignment) do |c_ids, t_ids| # Assign TAs in a round-robin fashion to a list of random criteria. + next [] if t_ids.empty? shuffled_criterion_ids = c_ids.shuffle shuffled_criterion_ids.zip(t_ids.cycle).map(&:flatten) end diff --git a/app/models/exam_template.rb b/app/models/exam_template.rb index 687ecd0836..b66defcccb 100644 --- a/app/models/exam_template.rb +++ b/app/models/exam_template.rb @@ -40,6 +40,8 @@ class ExamTemplate < ApplicationRecord length: { maximum: 20 } validates :num_pages, numericality: { greater_than_or_equal_to: 0, only_integer: true } + validates :automatic_parsing, inclusion: { in: [true, false] } + validates :cover_fields, exclusion: { in: [nil] } has_many :split_pdf_logs, dependent: :destroy has_many :template_divisions, dependent: :destroy diff --git a/app/models/extension.rb b/app/models/extension.rb index eb5a70cc86..bcf2503bb7 100644 --- a/app/models/extension.rb +++ b/app/models/extension.rb @@ -27,7 +27,8 @@ class Extension < ApplicationRecord attribute :time_delta, :interval - validates :time_delta, numericality: { greater_than: 0 } + validates :time_delta, presence: true, numericality: { greater_than: 0 } + validates :apply_penalty, inclusion: { in: [true, false] } after_create :validate_grouping diff --git a/app/models/grade_entry_item.rb b/app/models/grade_entry_item.rb index d251e1df47..b1d2cb12e1 100755 --- a/app/models/grade_entry_item.rb +++ b/app/models/grade_entry_item.rb @@ -38,6 +38,7 @@ class GradeEntryItem < ApplicationRecord validates :position, presence: true validates :position, numericality: { greater_than_or_equal_to: 0 } + validates :bonus, inclusion: { in: [true, false] } BLANK_MARK = ''.freeze diff --git a/app/models/grade_entry_student.rb b/app/models/grade_entry_student.rb index c0d9067ee1..a191fe8321 100644 --- a/app/models/grade_entry_student.rb +++ b/app/models/grade_entry_student.rb @@ -74,6 +74,7 @@ def self.randomly_assign_tas(student_ids, ta_ids, form) assign_tas(student_ids, ta_ids, form) do |grade_entry_student_ids, tids| # Assign TAs in a round-robin fashion to a list of random grade entry # students. + next [] if tids.empty? grade_entry_student_ids.shuffle.zip(tids.cycle) end end diff --git a/app/models/grader_permission.rb b/app/models/grader_permission.rb index f5ea8d1970..67cbf78c6e 100644 --- a/app/models/grader_permission.rb +++ b/app/models/grader_permission.rb @@ -22,4 +22,8 @@ class GraderPermission < ApplicationRecord belongs_to :ta, class_name: 'Ta', foreign_key: :role_id, inverse_of: :grader_permission has_one :course, through: :ta + + validates :manage_assessments, inclusion: { in: [true, false] } + validates :manage_submissions, inclusion: { in: [true, false] } + validates :run_tests, inclusion: { in: [true, false] } end diff --git a/app/models/grouping.rb b/app/models/grouping.rb index fe8bfd2296..1981ae7407 100644 --- a/app/models/grouping.rb +++ b/app/models/grouping.rb @@ -124,6 +124,8 @@ class Grouping < ApplicationRecord has_one :course, through: :assignment validates :is_collected, inclusion: { in: [true, false] } + validates :instructor_approved, inclusion: { in: [true, false] } + validates :starter_file_changed, inclusion: { in: [true, false] } validates :test_tokens, presence: true validates :test_tokens, numericality: { greater_than_or_equal_to: 0, only_integer: true } diff --git a/app/models/lti_client.rb b/app/models/lti_client.rb index 83293acba8..866c5f482c 100644 --- a/app/models/lti_client.rb +++ b/app/models/lti_client.rb @@ -18,7 +18,8 @@ class LtiClient < ApplicationRecord has_many :lti_deployments has_many :lti_users - validates :client_id, uniqueness: { scope: :host } + validates :client_id, presence: true, uniqueness: { scope: :host } + validates :host, presence: true include Rails.application.routes.url_helpers KEY_PATH = File.join(Settings.file_storage.lti || File.join(Settings.file_storage.default_root_path, 'lti', diff --git a/app/models/lti_deployment.rb b/app/models/lti_deployment.rb index 0b6642c419..e760095c04 100644 --- a/app/models/lti_deployment.rb +++ b/app/models/lti_deployment.rb @@ -26,7 +26,9 @@ class LtiDeployment < ApplicationRecord belongs_to :lti_client has_many :lti_services, dependent: :destroy has_many :lti_line_items, dependent: :destroy - validates :lms_course_id, uniqueness: { scope: :lti_client }, allow_nil: true + validates :external_deployment_id, presence: true + validates :lms_course_id, presence: true, uniqueness: { scope: :lti_client } + validates :lms_course_name, presence: true # See LTI documentation for full lists of scopes/claims/roles # https://www.imsglobal.org/spec/lti/v1p3 LTI_SCOPES = { names_role: 'https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly', diff --git a/app/models/lti_line_item.rb b/app/models/lti_line_item.rb index b921a25003..f57c16b1d3 100644 --- a/app/models/lti_line_item.rb +++ b/app/models/lti_line_item.rb @@ -26,4 +26,5 @@ class LtiLineItem < ApplicationRecord belongs_to :assessment belongs_to :lti_deployment validates :assessment, uniqueness: { scope: :lti_deployment_id } + validates :lti_line_item_id, presence: true end diff --git a/app/models/lti_service.rb b/app/models/lti_service.rb index 985885d837..550e4c4bec 100644 --- a/app/models/lti_service.rb +++ b/app/models/lti_service.rb @@ -28,4 +28,5 @@ class LtiService < ApplicationRecord belongs_to :lti_deployment validates :service_type, inclusion: { in: LTI_SERVICES.values } validates :service_type, uniqueness: { scope: :lti_deployment_id } + validates :url, presence: true end diff --git a/app/models/lti_user.rb b/app/models/lti_user.rb index 5bd4c7605e..f28dc0ba34 100644 --- a/app/models/lti_user.rb +++ b/app/models/lti_user.rb @@ -20,5 +20,5 @@ class LtiUser < ApplicationRecord belongs_to :lti_client belongs_to :user - validates :lti_user_id, uniqueness: { scope: :lti_client_id } + validates :lti_user_id, presence: true, uniqueness: { scope: :lti_client_id } end diff --git a/app/models/result.rb b/app/models/result.rb index b776464f79..8174db8a0b 100644 --- a/app/models/result.rb +++ b/app/models/result.rb @@ -47,6 +47,7 @@ class Result < ApplicationRecord validates :marking_state, inclusion: { in: MARKING_STATES.values } validates :released_to_students, inclusion: { in: [true, false] } + validates :view_token, presence: true before_update :check_for_released diff --git a/app/models/role.rb b/app/models/role.rb index 69156217fe..d58eb6f686 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -52,7 +52,11 @@ class Role < ApplicationRecord has_many :last_updated_grades, class_name: 'Grade', foreign_key: 'last_updated_by_id', inverse_of: :last_updated_by, dependent: :nullify - validates :type, format: { with: /\AStudent|Instructor|Ta|AdminRole\z/ } + validates :type, presence: true, format: { with: /\AStudent|Instructor|Ta|AdminRole\z/ } + validates :grace_credits, presence: true + validates :hidden, inclusion: { in: [true, false] } + validates :receives_invite_emails, inclusion: { in: [true, false] } + validates :receives_results_emails, inclusion: { in: [true, false] } validates :user_id, uniqueness: { scope: :course_id } after_update_commit :update_repo_permissions diff --git a/app/models/split_pdf_log.rb b/app/models/split_pdf_log.rb index 6d69edbf5a..8b4b1dfdec 100644 --- a/app/models/split_pdf_log.rb +++ b/app/models/split_pdf_log.rb @@ -39,6 +39,7 @@ class SplitPdfLog < ApplicationRecord validates :num_groups_in_complete, :num_groups_in_complete, :num_pages_qr_scan_error, :original_num_pages, numericality: { greater_than_or_equal_to: 0, only_integer: true } + validates :qr_code_found, inclusion: { in: [true, false] } validate :courses_should_match diff --git a/app/models/starter_file_entry.rb b/app/models/starter_file_entry.rb index abc78a5259..4a9bb126d5 100644 --- a/app/models/starter_file_entry.rb +++ b/app/models/starter_file_entry.rb @@ -20,6 +20,7 @@ class StarterFileEntry < ApplicationRecord belongs_to :starter_file_group has_one :course, through: :starter_file_group + validates :path, presence: true validate :entry_exists, if: :starter_file_group has_many :grouping_starter_file_entries, dependent: :destroy diff --git a/app/models/starter_file_group.rb b/app/models/starter_file_group.rb index 9e50293e3d..3495c9a66c 100644 --- a/app/models/starter_file_group.rb +++ b/app/models/starter_file_group.rb @@ -39,6 +39,7 @@ class StarterFileGroup < ApplicationRecord validates :entry_rename, exclusion: { in: %w[.. .] } validates :entry_rename, presence: { if: -> { self.use_rename } } + validates :use_rename, inclusion: { in: [true, false] } def path Pathname.new(assignment.starter_file_path) + id.to_s diff --git a/app/models/student.rb b/app/models/student.rb index b63c2b60cf..a05e8c0f65 100644 --- a/app/models/student.rb +++ b/app/models/student.rb @@ -70,10 +70,6 @@ class Student < Role validates :section, presence: { unless: -> { section_id.nil? } } - validates :receives_invite_emails, inclusion: { in: [true, false] } - - validates :receives_results_emails, inclusion: { in: [true, false] } - validates :grace_credits, numericality: { only_integer: true, greater_than_or_equal_to: 0 } @@ -149,7 +145,7 @@ def create_group_for_working_alone_student(aid) if @assignment.is_timed # must use a new group for timed assignments so that repos are # not accessible before the student starts the timer - @group = Group.new(course: @assignment.course) + @group = Group.new(course: @assignment.course, group_name: SecureRandom.uuid) @group.save(validate: false) @group.group_name = @group.get_autogenerated_group_name else @@ -188,9 +184,9 @@ def create_group_for_working_alone_student(aid) def create_autogenerated_name_group(assignment) Group.transaction do - group = Group.new(course: assignment.course) + group = Group.new(course: assignment.course, group_name: SecureRandom.uuid) group.save(validate: false) - group.group_name = group.get_autogenerated_group_name # the autogen name depends on the id, hence the two saves + group.group_name = group.get_autogenerated_group_name group.save grouping = Grouping.create(assignment: assignment, group_id: group.id) StudentMembership.create(grouping_id: grouping.id, membership_status: StudentMembership::STATUSES[:inviter], diff --git a/app/models/submission.rb b/app/models/submission.rb index b781f5f610..b5df6e449f 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -29,6 +29,7 @@ class Submission < ApplicationRecord validates :submission_version_used, inclusion: { in: [true, false] } validates :submission_version, numericality: { only_integer: true } + validates :is_empty, inclusion: { in: [true, false] } validate :max_number_of_results belongs_to :grouping diff --git a/app/models/submission_file.rb b/app/models/submission_file.rb index 8e6b0502fe..6851b45ea1 100644 --- a/app/models/submission_file.rb +++ b/app/models/submission_file.rb @@ -33,6 +33,7 @@ class SubmissionFile < ApplicationRecord validates :path, presence: true validates :is_converted, inclusion: { in: [true, false] } + validates :error_converting, inclusion: { in: [true, false] } def is_supported_image? # Here you can add more image types to support diff --git a/app/models/template_division.rb b/app/models/template_division.rb index 2d18016f41..ad9684386b 100644 --- a/app/models/template_division.rb +++ b/app/models/template_division.rb @@ -31,14 +31,14 @@ class TemplateDivision < ApplicationRecord accepts_nested_attributes_for :assignment_file, allow_destroy: true validate :courses_should_match - validates :start, numericality: { greater_than_or_equal_to: 2, - less_than_or_equal_to: :end, - only_integer: true } - validates :end, numericality: { only_integer: true } + validates :start, presence: true, numericality: { greater_than_or_equal_to: 2, + less_than_or_equal_to: :end, + only_integer: true } + validates :end, presence: true, numericality: { only_integer: true } validate :end_should_be_less_than_or_equal_to_num_pages - validates :label, - uniqueness: { scope: :exam_template, - allow_blank: false } + validates :label, presence: true, + uniqueness: { scope: :exam_template, + allow_blank: false } after_save :set_defaults_for_assignment_file # when template division is created or updated diff --git a/app/models/test_group.rb b/app/models/test_group.rb index a8e228846b..670b1e542e 100644 --- a/app/models/test_group.rb +++ b/app/models/test_group.rb @@ -35,6 +35,8 @@ class TestGroup < ApplicationRecord validates :name, presence: true validates :display_output, presence: true + validates :autotest_settings, exclusion: { in: [nil] } + validates :position, presence: true validate :courses_should_match def to_json(*_args) diff --git a/app/models/test_result.rb b/app/models/test_result.rb index c76e33e214..d74cf97858 100644 --- a/app/models/test_result.rb +++ b/app/models/test_result.rb @@ -31,6 +31,7 @@ class TestResult < ApplicationRecord validates :name, presence: true, uniqueness: { scope: :test_group_result } validates :status, presence: true, inclusion: { in: %w[pass partial fail error error_all] } validates :marks_earned, :marks_total, presence: true, numericality: { greater_than_or_equal_to: 0 } + validates :position, presence: true validates :time, numericality: { greater_than_or_equal_to: 0, only_integer: true, allow_nil: true } # output could be empty in some situations validates :output, presence: true, if: ->(o) { o.output.nil? } diff --git a/app/models/test_run.rb b/app/models/test_run.rb index 4cc18420b0..1f941dd941 100644 --- a/app/models/test_run.rb +++ b/app/models/test_run.rb @@ -41,6 +41,7 @@ class TestRun < ApplicationRecord has_one :course, through: :role + validates :status, presence: true validate :courses_should_match validate :autotest_test_id_uniqueness before_save :unset_autotest_test_id diff --git a/app/models/user.rb b/app/models/user.rb index 55dedb7175..0817ac22ba 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -45,7 +45,7 @@ class User < ApplicationRecord has_many :lti_users, dependent: :destroy validates :type, format: { with: /\AEndUser|AutotestUser|AdminUser\z/ } - validates :user_name, :last_name, :first_name, :time_zone, :display_name, presence: true + validates :user_name, :last_name, :first_name, :time_zone, :display_name, :theme, presence: true validates :user_name, uniqueness: true validates :email, uniqueness: { allow_nil: true } validates :id_number, uniqueness: { allow_nil: true } diff --git a/db/migrate/20260526021351_enforce_not_null_constraints.rb b/db/migrate/20260526021351_enforce_not_null_constraints.rb new file mode 100644 index 0000000000..a9fe98a057 --- /dev/null +++ b/db/migrate/20260526021351_enforce_not_null_constraints.rb @@ -0,0 +1,131 @@ +class EnforceNotNullConstraints < ActiveRecord::Migration[8.1] + def change + change_column_null :users, :first_name, false + change_column_null :users, :last_name, false + change_column_null :users, :created_at, false + change_column_null :users, :updated_at, false + + change_column_null :annotations, :annotation_text_id, false + change_column_null :annotations, :submission_file_id, false + change_column_null :annotations, :annotation_number, false + change_column_null :annotations, :result_id, false + + change_column_null :annotation_categories, :annotation_category_name, false + change_column_null :annotation_categories, :created_at, false + change_column_null :annotation_categories, :updated_at, false + + change_column_null :annotation_texts, :created_at, false + change_column_null :annotation_texts, :updated_at, false + + change_column_null :assessment_section_properties, :section_id, false + change_column_null :assessment_section_properties, :assessment_id, false + + change_column_null :assignment_files, :created_at, false + change_column_null :assignment_files, :updated_at, false + change_column_null :assignment_files, :assessment_id, false + + change_column_null :assignment_properties, :assessment_id, false + + change_column_null :criteria_assignment_files_joins, :created_at, false + change_column_null :criteria_assignment_files_joins, :updated_at, false + + change_column_null :criterion_ta_associations, :ta_id, false + change_column_null :criterion_ta_associations, :criterion_id, false + change_column_null :criterion_ta_associations, :created_at, false + change_column_null :criterion_ta_associations, :updated_at, false + change_column_null :criterion_ta_associations, :assessment_id, false + + change_column_null :exam_templates, :assessment_id, false + + change_column_null :extra_marks, :result_id, false + change_column_null :extra_marks, :created_at, false + change_column_null :extra_marks, :updated_at, false + change_column_null :extra_marks, :unit, false + + change_column_null :grace_period_deductions, :created_at, false + change_column_null :grace_period_deductions, :updated_at, false + + change_column_null :submission_rules, :created_at, false + change_column_null :submission_rules, :updated_at, false + + change_column_null :grades, :grade_entry_item_id, false + change_column_null :grades, :grade_entry_student_id, false + change_column_null :grades, :created_at, false + change_column_null :grades, :updated_at, false + + change_column_null :grade_entry_items, :created_at, false + change_column_null :grade_entry_items, :updated_at, false + change_column_null :grade_entry_items, :out_of, false + change_column_null :grade_entry_items, :position, false + change_column_null :grade_entry_items, :assessment_id, false + + change_column_null :grade_entry_students, :created_at, false + change_column_null :grade_entry_students, :updated_at, false + change_column_null :grade_entry_students, :assessment_id, false + + change_column_null :grade_entry_students_tas, :grade_entry_student_id, false + change_column_null :grade_entry_students_tas, :ta_id, false + + change_column_null :groups, :group_name, false + + change_column_null :groupings, :created_at, false + change_column_null :groupings, :updated_at, false + + change_column_null :key_pairs, :user_id, false + change_column_null :key_pairs, :public_key, false + change_column_null :key_pairs, :created_at, false + change_column_null :key_pairs, :updated_at, false + + change_column_null :marks, :result_id, false + change_column_null :marks, :criterion_id, false + change_column_null :marks, :created_at, false + change_column_null :marks, :updated_at, false + + change_column_null :marking_schemes, :created_at, false + change_column_null :marking_schemes, :updated_at, false + + change_column_null :marking_weights, :marking_scheme_id, false + change_column_null :marking_weights, :created_at, false + change_column_null :marking_weights, :updated_at, false + + change_column_null :memberships, :created_at, false + change_column_null :memberships, :updated_at, false + + change_column_null :notes, :created_at, false + change_column_null :notes, :updated_at, false + + change_column_null :periods, :submission_rule_id, false + change_column_null :periods, :created_at, false + change_column_null :periods, :updated_at, false + change_column_null :periods, :submission_rule_type, false + + change_column_null :results, :submission_id, false + change_column_null :results, :marking_state, false + change_column_null :results, :created_at, false + change_column_null :results, :updated_at, false + + change_column_null :sections, :name, false + change_column_null :sections, :created_at, false + change_column_null :sections, :updated_at, false + + change_column_null :split_pages, :split_pdf_log_id, false + + change_column_null :split_pdf_logs, :filename, false + change_column_null :split_pdf_logs, :num_groups_in_complete, false + change_column_null :split_pdf_logs, :num_groups_in_incomplete, false + change_column_null :split_pdf_logs, :num_pages_qr_scan_error, false + change_column_null :split_pdf_logs, :original_num_pages, false + change_column_null :split_pdf_logs, :exam_template_id, false + + change_column_null :submissions, :grouping_id, false + change_column_null :submissions, :created_at, false + + change_column_null :submission_files, :submission_id, false + change_column_null :submission_files, :filename, false + + change_column_null :template_divisions, :exam_template_id, false + + change_column_null :test_results, :created_at, false + change_column_null :test_results, :updated_at, false + end +end diff --git a/db/structure.sql b/db/structure.sql index 04a666dda0..0218537ff2 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -1,6 +1,7 @@ SET statement_timeout = 0; SET lock_timeout = 0; SET idle_in_transaction_session_timeout = 0; +SET transaction_timeout = 0; SET client_encoding = 'UTF8'; SET standard_conforming_strings = on; SELECT pg_catalog.set_config('search_path', '', false); @@ -144,10 +145,10 @@ SET default_table_access_method = heap; CREATE TABLE public.annotation_categories ( id integer NOT NULL, - annotation_category_name text, + annotation_category_name text NOT NULL, "position" integer, - created_at timestamp without time zone, - updated_at timestamp without time zone, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, assessment_id bigint NOT NULL, flexible_criterion_id bigint ); @@ -181,8 +182,8 @@ CREATE TABLE public.annotation_texts ( id integer NOT NULL, content text, annotation_category_id integer, - created_at timestamp without time zone, - updated_at timestamp without time zone, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, creator_id integer, last_editor_id integer, deduction double precision @@ -217,21 +218,21 @@ CREATE TABLE public.annotations ( id integer NOT NULL, line_start integer, line_end integer, - annotation_text_id integer, - submission_file_id integer, + annotation_text_id integer NOT NULL, + submission_file_id integer NOT NULL, x1 integer, x2 integer, y1 integer, y2 integer, type character varying, - annotation_number integer, + annotation_number integer NOT NULL, is_remark boolean DEFAULT false NOT NULL, page integer, column_start integer, column_end integer, creator_type character varying, creator_id integer, - result_id integer, + result_id integer NOT NULL, start_node character varying, end_node character varying, start_offset integer, @@ -278,8 +279,8 @@ CREATE TABLE public.ar_internal_metadata ( CREATE TABLE public.assessment_section_properties ( id integer NOT NULL, due_date timestamp without time zone, - section_id integer, - assessment_id bigint, + section_id integer NOT NULL, + assessment_id bigint NOT NULL, start_time timestamp without time zone, is_hidden boolean, visible_on timestamp(6) without time zone, @@ -356,9 +357,9 @@ ALTER SEQUENCE public.assessments_id_seq OWNED BY public.assessments.id; CREATE TABLE public.assignment_files ( id integer NOT NULL, filename character varying NOT NULL, - created_at timestamp without time zone, - updated_at timestamp without time zone, - assessment_id bigint + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + assessment_id bigint NOT NULL ); @@ -388,7 +389,7 @@ ALTER SEQUENCE public.assignment_files_id_seq OWNED BY public.assignment_files.i CREATE TABLE public.assignment_properties ( id integer NOT NULL, - assessment_id bigint, + assessment_id bigint NOT NULL, group_min integer DEFAULT 1 NOT NULL, group_max integer DEFAULT 1 NOT NULL, student_form_groups boolean DEFAULT false NOT NULL, @@ -551,8 +552,8 @@ CREATE TABLE public.criteria_assignment_files_joins ( id integer NOT NULL, criterion_id integer NOT NULL, assignment_file_id integer NOT NULL, - created_at timestamp without time zone, - updated_at timestamp without time zone + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL ); @@ -601,11 +602,11 @@ ALTER SEQUENCE public.criteria_id_seq OWNED BY public.criteria.id; CREATE TABLE public.criterion_ta_associations ( id integer NOT NULL, - ta_id integer, - criterion_id integer, - created_at timestamp without time zone, - updated_at timestamp without time zone, - assessment_id bigint + ta_id integer NOT NULL, + criterion_id integer NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + assessment_id bigint NOT NULL ); @@ -646,7 +647,7 @@ CREATE TABLE public.exam_templates ( crop_y numeric, crop_width numeric, crop_height numeric, - assessment_id bigint + assessment_id bigint NOT NULL ); @@ -710,12 +711,12 @@ ALTER SEQUENCE public.extensions_id_seq OWNED BY public.extensions.id; CREATE TABLE public.extra_marks ( id integer NOT NULL, - result_id integer, + result_id integer NOT NULL, description character varying, extra_mark double precision, - created_at timestamp without time zone, - updated_at timestamp without time zone, - unit character varying + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + unit character varying NOT NULL ); @@ -783,8 +784,8 @@ CREATE TABLE public.grace_period_deductions ( id integer NOT NULL, membership_id integer, deduction integer, - created_at timestamp without time zone, - updated_at timestamp without time zone + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL ); @@ -815,12 +816,12 @@ ALTER SEQUENCE public.grace_period_deductions_id_seq OWNED BY public.grace_perio CREATE TABLE public.grade_entry_items ( id integer NOT NULL, name character varying NOT NULL, - created_at timestamp without time zone, - updated_at timestamp without time zone, - out_of double precision, - "position" integer, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + out_of double precision NOT NULL, + "position" integer NOT NULL, bonus boolean DEFAULT false NOT NULL, - assessment_id bigint + assessment_id bigint NOT NULL ); @@ -851,9 +852,9 @@ ALTER SEQUENCE public.grade_entry_items_id_seq OWNED BY public.grade_entry_items CREATE TABLE public.grade_entry_students ( id integer NOT NULL, released_to_student boolean DEFAULT false NOT NULL, - created_at timestamp without time zone, - updated_at timestamp without time zone, - assessment_id bigint, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + assessment_id bigint NOT NULL, role_id bigint NOT NULL ); @@ -883,8 +884,8 @@ ALTER SEQUENCE public.grade_entry_students_id_seq OWNED BY public.grade_entry_st -- CREATE TABLE public.grade_entry_students_tas ( - grade_entry_student_id integer, - ta_id integer, + grade_entry_student_id integer NOT NULL, + ta_id integer NOT NULL, id integer NOT NULL ); @@ -947,11 +948,11 @@ ALTER SEQUENCE public.grader_permissions_id_seq OWNED BY public.grader_permissio CREATE TABLE public.grades ( id integer NOT NULL, - grade_entry_item_id integer, - grade_entry_student_id integer, + grade_entry_item_id integer NOT NULL, + grade_entry_student_id integer NOT NULL, grade double precision, - created_at timestamp without time zone, - updated_at timestamp without time zone, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, last_updated_by_id bigint ); @@ -1013,8 +1014,8 @@ ALTER SEQUENCE public.grouping_starter_file_entries_id_seq OWNED BY public.group CREATE TABLE public.groupings ( id integer NOT NULL, group_id integer NOT NULL, - created_at timestamp without time zone, - updated_at timestamp without time zone, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, instructor_approved boolean DEFAULT false NOT NULL, is_collected boolean DEFAULT false NOT NULL, criteria_coverage_count integer DEFAULT 0, @@ -1061,7 +1062,7 @@ CREATE TABLE public.groupings_tags ( CREATE TABLE public.groups ( id integer NOT NULL, - group_name character varying, + group_name character varying NOT NULL, repo_name character varying, course_id bigint NOT NULL ); @@ -1127,10 +1128,10 @@ ALTER SEQUENCE public.job_messengers_id_seq OWNED BY public.job_messengers.id; CREATE TABLE public.key_pairs ( id integer NOT NULL, - user_id integer, - public_key character varying, - created_at timestamp without time zone, - updated_at timestamp without time zone + user_id integer NOT NULL, + public_key character varying NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL ); @@ -1365,8 +1366,8 @@ ALTER SEQUENCE public.lti_users_id_seq OWNED BY public.lti_users.id; CREATE TABLE public.marking_schemes ( id integer NOT NULL, name character varying, - created_at timestamp without time zone, - updated_at timestamp without time zone, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, course_id bigint NOT NULL ); @@ -1397,10 +1398,10 @@ ALTER SEQUENCE public.marking_schemes_id_seq OWNED BY public.marking_schemes.id; CREATE TABLE public.marking_weights ( id integer NOT NULL, - marking_scheme_id integer, + marking_scheme_id integer NOT NULL, weight numeric, - created_at timestamp without time zone, - updated_at timestamp without time zone, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, assessment_id bigint NOT NULL ); @@ -1431,11 +1432,11 @@ ALTER SEQUENCE public.marking_weights_id_seq OWNED BY public.marking_weights.id; CREATE TABLE public.marks ( id integer NOT NULL, - result_id integer, - criterion_id integer, + result_id integer NOT NULL, + criterion_id integer NOT NULL, mark double precision, - created_at timestamp without time zone, - updated_at timestamp without time zone, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, override boolean DEFAULT false NOT NULL, last_updated_by_id bigint ); @@ -1468,8 +1469,8 @@ ALTER SEQUENCE public.marks_id_seq OWNED BY public.marks.id; CREATE TABLE public.memberships ( id integer NOT NULL, membership_status character varying, - created_at timestamp without time zone, - updated_at timestamp without time zone, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, grouping_id integer NOT NULL, type character varying, role_id bigint NOT NULL @@ -1504,8 +1505,8 @@ CREATE TABLE public.notes ( id integer NOT NULL, notes_message text NOT NULL, creator_id integer NOT NULL, - created_at timestamp without time zone, - updated_at timestamp without time zone, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, noteable_type character varying NOT NULL, noteable_id integer NOT NULL ); @@ -1570,13 +1571,13 @@ ALTER SEQUENCE public.peer_reviews_id_seq OWNED BY public.peer_reviews.id; CREATE TABLE public.periods ( id integer NOT NULL, - submission_rule_id integer, + submission_rule_id integer NOT NULL, deduction double precision, - created_at timestamp without time zone, - updated_at timestamp without time zone, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, hours double precision, "interval" double precision, - submission_rule_type character varying + submission_rule_type character varying NOT NULL ); @@ -1606,11 +1607,11 @@ ALTER SEQUENCE public.periods_id_seq OWNED BY public.periods.id; CREATE TABLE public.results ( id integer NOT NULL, - submission_id integer, - marking_state character varying, + submission_id integer NOT NULL, + marking_state character varying NOT NULL, overall_comment text, - created_at timestamp without time zone, - updated_at timestamp without time zone, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, released_to_students boolean DEFAULT false NOT NULL, remark_request_submitted_at timestamp without time zone, view_token character varying NOT NULL, @@ -1721,9 +1722,9 @@ ALTER SEQUENCE public.section_starter_file_groups_id_seq OWNED BY public.section CREATE TABLE public.sections ( id integer NOT NULL, - name character varying, - created_at timestamp without time zone, - updated_at timestamp without time zone, + name character varying NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, course_id bigint NOT NULL ); @@ -1791,7 +1792,7 @@ CREATE TABLE public.split_pages ( exam_page_number integer, filename character varying, status character varying, - split_pdf_log_id integer, + split_pdf_log_id integer NOT NULL, group_id integer, created_at timestamp without time zone NOT NULL, updated_at timestamp without time zone NOT NULL @@ -1826,15 +1827,15 @@ CREATE TABLE public.split_pdf_logs ( id integer NOT NULL, uploaded_when timestamp without time zone, error_description character varying, - filename character varying, - num_groups_in_complete integer, - num_groups_in_incomplete integer, - num_pages_qr_scan_error integer, - original_num_pages integer, + filename character varying NOT NULL, + num_groups_in_complete integer NOT NULL, + num_groups_in_incomplete integer NOT NULL, + num_pages_qr_scan_error integer NOT NULL, + original_num_pages integer NOT NULL, qr_code_found boolean DEFAULT false NOT NULL, created_at timestamp without time zone NOT NULL, updated_at timestamp without time zone NOT NULL, - exam_template_id integer, + exam_template_id integer NOT NULL, role_id bigint NOT NULL ); @@ -1927,8 +1928,8 @@ ALTER SEQUENCE public.starter_file_groups_id_seq OWNED BY public.starter_file_gr CREATE TABLE public.submission_files ( id integer NOT NULL, - submission_id integer, - filename character varying, + submission_id integer NOT NULL, + filename character varying NOT NULL, path character varying DEFAULT '/'::character varying NOT NULL, is_converted boolean DEFAULT false NOT NULL, error_converting boolean DEFAULT false NOT NULL @@ -1962,8 +1963,8 @@ ALTER SEQUENCE public.submission_files_id_seq OWNED BY public.submission_files.i CREATE TABLE public.submission_rules ( id integer NOT NULL, type character varying DEFAULT 'NoLateSubmissionRule'::character varying, - created_at timestamp without time zone, - updated_at timestamp without time zone, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, assessment_id bigint NOT NULL, penalty_type character varying DEFAULT 'percentage'::character varying ); @@ -1995,8 +1996,8 @@ ALTER SEQUENCE public.submission_rules_id_seq OWNED BY public.submission_rules.i CREATE TABLE public.submissions ( id integer NOT NULL, - grouping_id integer, - created_at timestamp without time zone, + grouping_id integer NOT NULL, + created_at timestamp without time zone NOT NULL, submission_version integer, submission_version_used boolean DEFAULT false NOT NULL, revision_identifier text, @@ -2066,7 +2067,7 @@ ALTER SEQUENCE public.tags_id_seq OWNED BY public.tags.id; CREATE TABLE public.template_divisions ( id integer NOT NULL, - exam_template_id integer, + exam_template_id integer NOT NULL, start integer NOT NULL, "end" integer NOT NULL, label character varying NOT NULL, @@ -2213,8 +2214,8 @@ CREATE TABLE public.test_results ( status text NOT NULL, marks_earned double precision DEFAULT 0.0 NOT NULL, output text DEFAULT ''::text NOT NULL, - created_at timestamp without time zone, - updated_at timestamp without time zone, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, marks_total double precision DEFAULT 0.0 NOT NULL, "time" bigint, test_group_result_id bigint NOT NULL, @@ -2288,11 +2289,11 @@ ALTER SEQUENCE public.test_runs_id_seq OWNED BY public.test_runs.id; CREATE TABLE public.users ( id integer NOT NULL, user_name character varying NOT NULL, - last_name character varying, - first_name character varying, + last_name character varying NOT NULL, + first_name character varying NOT NULL, type character varying, - created_at timestamp without time zone, - updated_at timestamp without time zone, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, api_key character varying, email character varying, id_number character varying, @@ -4453,6 +4454,7 @@ ALTER TABLE ONLY public.submission_files SET search_path TO "$user", public; INSERT INTO "schema_migrations" (version) VALUES +('20260526021351'), ('20260415150142'), ('20260326174749'), ('20260325180720'), diff --git a/spec/models/annotation_text_spec.rb b/spec/models/annotation_text_spec.rb index ceb354dce2..d523c0765a 100644 --- a/spec/models/annotation_text_spec.rb +++ b/spec/models/annotation_text_spec.rb @@ -15,7 +15,7 @@ it { is_expected.to belong_to(:annotation_category).optional } it { is_expected.to have_many(:annotations) } - it { is_expected.to belong_to(:creator) } + it { is_expected.to belong_to(:creator).optional } it { is_expected.to belong_to(:last_editor).optional } it { is_expected.to have_one(:course) } diff --git a/spec/support/criterion.rb b/spec/support/criterion.rb index 40fb2daf29..a8fef1b9a6 100644 --- a/spec/support/criterion.rb +++ b/spec/support/criterion.rb @@ -245,7 +245,7 @@ def expect_updated_assigned_groups_count_to_eq(expected_count) # Creating a new criterion also creates a new assignment. criterion = create(criterion_factory_name) grouping = create(:grouping, assignment: criterion.assignment) - Criterion.assign_all_tas([[criterion.id, criterion.class.to_s]], tas.map(&:id), criterion.assignment) + Criterion.assign_all_tas([criterion.id], tas.map(&:id), criterion.assignment) create_ta_memberships(grouping, tas) end