diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 6268621a0..0fc47dee4 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,29 +1,11 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2022-12-29 09:30:33 UTC using RuboCop version 1.41.1. +# on 2026-01-17 13:44:49 UTC using RuboCop version 1.75.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, IndentationWidth. -# SupportedStyles: outdent, indent -Layout/AccessModifierIndentation: - Exclude: - - 'app/models/unit.rb' - -# Offense count: 10 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, IndentationWidth. -# SupportedStyles: with_first_argument, with_fixed_indentation -Layout/ArgumentAlignment: - Exclude: - - 'app/api/activity_types_authenticated_api.rb' - - 'app/api/authentication_api.rb' - - 'app/api/campuses_authenticated_api.rb' - # Offense count: 3 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, IndentationWidth. @@ -32,90 +14,25 @@ Layout/ArrayAlignment: Exclude: - 'app/helpers/file_helper.rb' -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: IndentationWidth. -Layout/AssignmentIndentation: - Exclude: - - 'lib/tasks/init.rake' - -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyleAlignWith. -# SupportedStylesAlignWith: either, start_of_block, start_of_line -Layout/BlockAlignment: - Exclude: - - 'app/models/project.rb' - - 'config/deakin.rb' - -# Offense count: 19 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, IndentOneStep, IndentationWidth. -# SupportedStyles: case, end -Layout/CaseIndentation: - Exclude: - - 'app/models/task_status.rb' - - 'app/models/webcal.rb' - - 'config/deakin.rb' - -# Offense count: 4 -# This cop supports safe autocorrection (--autocorrect). -Layout/ClosingParenthesisIndentation: - Exclude: - - 'app/api/units_api.rb' - - 'app/models/unit.rb' - - 'config/deakin.rb' - -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowForAlignment. -Layout/CommentIndentation: - Exclude: - - 'config/initializers/inflections.rb' - -# Offense count: 76 +# Offense count: 8 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: leading, trailing Layout/DotPosition: Exclude: - - 'app/api/group_sets_api.rb' - - 'app/api/task_definitions_api.rb' - - 'app/api/tasks_api.rb' - - 'app/models/project.rb' - - 'app/models/task.rb' - - 'app/models/tutorial_enrolment.rb' - 'app/models/unit.rb' - - 'app/models/unit_role.rb' - - 'config/deakin.rb' - - 'lib/tasks/maintenance.rake' - -# Offense count: 3 -# This cop supports safe autocorrection (--autocorrect). -Layout/ElseAlignment: - Exclude: - - 'app/api/students_api.rb' - - 'app/models/task_definition.rb' -# Offense count: 67 +# Offense count: 28 # This cop supports safe autocorrection (--autocorrect). Layout/EmptyLineAfterGuardClause: Enabled: false -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -Layout/EmptyLineAfterMagicComment: - Exclude: - - 'app/api/discussion_comment_api.rb' - - 'app/models/comments/task_comment.rb' - # Offense count: 4 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EmptyLineBetweenMethodDefs, EmptyLineBetweenClassDefs, EmptyLineBetweenModuleDefs, AllowAdjacentOneLineDefs, NumberOfEmptyLines. +# Configuration parameters: EmptyLineBetweenMethodDefs, EmptyLineBetweenClassDefs, EmptyLineBetweenModuleDefs, DefLikeMacros, AllowAdjacentOneLineDefs, NumberOfEmptyLines. Layout/EmptyLineBetweenDefs: Exclude: - 'app/models/overseer_assessment.rb' - - 'app/models/role.rb' - 'app/models/unit.rb' # Offense count: 13 @@ -123,101 +40,35 @@ Layout/EmptyLineBetweenDefs: Layout/EmptyLines: Exclude: - 'app/api/submission/portfolio_evidence_api.rb' - - 'app/helpers/csv_helper.rb' - - 'app/models/auth_token.rb' - 'app/models/overseer_assessment.rb' - - 'app/models/role.rb' - 'app/models/unit.rb' - - 'config/environments/development.rb' - - 'lib/tasks/init.rake' -# Offense count: 9 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: around, only_before -Layout/EmptyLinesAroundAccessModifier: - Exclude: - - 'app/models/activity_type.rb' - - 'app/models/campus.rb' - - 'app/models/project.rb' - - 'app/models/task.rb' - - 'app/models/tutorial.rb' - - 'app/models/tutorial_enrolment.rb' - - 'app/models/tutorial_stream.rb' - - 'app/models/unit.rb' - -# Offense count: 6 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: empty_lines, no_empty_lines -Layout/EmptyLinesAroundBlockBody: - Exclude: - - 'app/api/submission/portfolio_evidence_api.rb' - - 'app/api/webcal_public_api.rb' - - 'app/models/tutorial_enrolment.rb' - - 'config/environments/production.rb' - - 'lib/tasks/init.rake' - - 'lib/tasks/send_status_emails.rake' - -# Offense count: 22 +# Offense count: 23 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines, beginning_only, ending_only Layout/EmptyLinesAroundClassBody: Enabled: false -# Offense count: 5 +# Offense count: 2 # This cop supports safe autocorrection (--autocorrect). Layout/EmptyLinesAroundExceptionHandlingKeywords: Exclude: - - 'app/helpers/file_helper.rb' - - 'app/models/overseer_assessment.rb' + - 'app/api/staff_grant_extension_api.rb' - 'app/models/task.rb' - - 'app/models/unit.rb' - - 'lib/assets/ontrack_receive_action.rb' -# Offense count: 2 +# Offense count: 1 # This cop supports safe autocorrection (--autocorrect). Layout/EmptyLinesAroundMethodBody: Exclude: - - 'app/models/portfolio_evidence.rb' - 'app/models/unit_role.rb' -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines -Layout/EmptyLinesAroundModuleBody: - Exclude: - - 'app/api/admin/overseer_admin_api.rb' - - 'app/helpers/authentication_helpers.rb' - -# Offense count: 5 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyleAlignWith, Severity. -# SupportedStylesAlignWith: keyword, variable, start_of_line -Layout/EndAlignment: - Exclude: - - 'app/api/students_api.rb' - - 'app/channels/application_cable/channel.rb' - - 'app/models/task_definition.rb' - - 'config/application.rb' - -# Offense count: 21 +# Offense count: 1 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowForAlignment, AllowBeforeTrailingComments, ForceEqualSignAlignment. Layout/ExtraSpacing: Exclude: - - 'app/api/entities/project_entity.rb' - - 'app/api/entities/unit_entity.rb' - - 'app/api/projects_api.rb' - - 'app/api/tutorial_streams_api.rb' - - 'app/helpers/mime_check_helpers.rb' - - 'app/mailers/notifications_mailer.rb' - - 'app/models/project.rb' - - 'app/models/task.rb' - - 'config/initializers/swagger.rb' - - 'lib/helpers/database_populator.rb' + - 'app/api/staff_grant_extension_api.rb' # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). @@ -227,251 +78,54 @@ Layout/FirstArrayElementIndentation: Exclude: - 'app/models/unit.rb' -# Offense count: 4 +# Offense count: 6 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, IndentationWidth. # SupportedStyles: special_inside_parentheses, consistent, align_braces Layout/FirstHashElementIndentation: Exclude: + - 'app/api/staff_grant_extension_api.rb' - 'app/models/unit.rb' - - 'config/no_institution_setting.rb' - - 'lib/helpers/database_populator.rb' - -# Offense count: 114 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. -# SupportedHashRocketStyles: key, separator, table -# SupportedColonStyles: key, separator, table -# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit -Layout/HashAlignment: - Exclude: - - 'app/api/authentication_api.rb' - - 'app/api/settings_api.rb' - - 'app/models/task.rb' - - 'app/models/teaching_period.rb' - - 'app/models/unit.rb' - - 'app/models/user.rb' - - 'config/deakin.rb' - - 'config/environments/production.rb' - - 'config/no_institution_setting.rb' - - 'lib/helpers/database_populator.rb' - - 'lib/helpers/find_or_create_students.rb' # Offense count: 9 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: normal, indented_internal_methods -Layout/IndentationConsistency: - Exclude: - - 'app/models/task.rb' - - 'app/models/task_definition.rb' - - 'app/models/tutorial_enrolment.rb' - - 'config/deakin.rb' - -# Offense count: 36 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Width, AllowedPatterns, IgnoredPatterns. +# Configuration parameters: Width, AllowedPatterns. Layout/IndentationWidth: Exclude: - - 'app/api/students_api.rb' - - 'app/channels/application_cable/channel.rb' - - 'app/mailers/notifications_mailer.rb' - - 'app/models/task.rb' - - 'app/models/task_definition.rb' - - 'app/models/tutorial_enrolment.rb' - - 'app/models/unit.rb' - - 'app/models/user.rb' - - 'config/application.rb' - - 'config/deakin.rb' - - 'config/initializers/inflections.rb' - - 'config/no_institution_setting.rb' - - 'lib/tasks/send_status_emails.rake' - - 'lib/tasks/skip_prod.rake' - -# Offense count: 38 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowDoxygenCommentStyle, AllowGemfileRubyComment. -Layout/LeadingCommentSpace: - Exclude: - - 'app/api/entities/group_entity.rb' - - 'app/api/entities/tutorial_entity.rb' - - 'app/api/entities/tutorial_stream_entity.rb' - - 'app/api/task_definitions_api.rb' - - 'app/models/overseer_assessment.rb' - - 'app/models/project.rb' - - 'app/models/task.rb' - 'app/models/task_definition.rb' - - 'app/models/task_status.rb' - - 'app/models/teaching_period.rb' - - 'app/models/unit.rb' - - 'config/deakin.rb' - - 'lib/helpers/database_populator.rb' - -# Offense count: 5 -# This cop supports safe autocorrection (--autocorrect). -Layout/LeadingEmptyLines: - Exclude: - - 'app/api/entities/minimal/minimal_unit_entity.rb' - - 'app/api/entities/minimal/minimal_user_entity.rb' - - 'app/api/entities/user_entity.rb' - - 'app/api/entities/webcal_entity.rb' - - 'config/no_institution_setting.rb' - -# Offense count: 16 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AutoCorrect, EnforcedStyle. -# SupportedStyles: leading, trailing -Layout/LineContinuationLeadingSpace: - Exclude: - - 'config/application.rb' - -# Offense count: 16 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect, EnforcedStyle. -# SupportedStyles: space, no_space -Layout/LineContinuationSpacing: - Exclude: - - 'config/application.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, IndentationWidth. -# SupportedStyles: aligned, indented -Layout/LineEndStringConcatenationIndentation: - Exclude: - - 'config/application.rb' - -# Offense count: 4 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: symmetrical, new_line, same_line -Layout/MultilineMethodCallBraceLayout: - Exclude: - - 'app/api/units_api.rb' - 'app/models/unit.rb' - - 'app/models/unit_role.rb' -# Offense count: 82 +# Offense count: 45 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, IndentationWidth. # SupportedStyles: aligned, indented, indented_relative_to_receiver Layout/MultilineMethodCallIndentation: Exclude: - - 'app/api/activity_types_authenticated_api.rb' - - 'app/api/admin/overseer_admin_api.rb' - - 'app/api/campuses_authenticated_api.rb' - - 'app/api/group_sets_api.rb' - - 'app/api/learning_outcomes_api.rb' - - 'app/api/task_definitions_api.rb' - - 'app/api/tasks_api.rb' - - 'app/api/teaching_periods_authenticated_api.rb' - - 'app/api/unit_roles_api.rb' - - 'app/api/webcal_api.rb' - - 'app/models/project.rb' - - 'app/models/tutorial_enrolment.rb' - 'app/models/unit.rb' - 'app/models/unit_role.rb' - - 'config/deakin.rb' - -# Offense count: 11 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, IndentationWidth. -# SupportedStyles: aligned, indented -Layout/MultilineOperationIndentation: - Exclude: - - 'app/api/authentication_api.rb' - - 'app/models/unit.rb' - - 'config/application.rb' - -# Offense count: 4 -# This cop supports safe autocorrection (--autocorrect). -Layout/SpaceAfterColon: - Exclude: - - 'app/api/extension_comments_api.rb' - - 'config/deakin.rb' -# Offense count: 46 +# Offense count: 35 # This cop supports safe autocorrection (--autocorrect). Layout/SpaceAfterComma: Exclude: - - 'app/api/api_root.rb' - - 'app/api/tutorial_streams_api.rb' - - 'app/api/units_api.rb' - - 'app/helpers/application_helper.rb' - 'app/helpers/file_helper.rb' - - 'app/models/activity_type.rb' - - 'app/models/campus.rb' - - 'app/models/task.rb' - - 'app/models/teaching_period.rb' - - 'config/deakin.rb' - - 'lib/helpers/database_populator.rb' -# Offense count: 7 -# This cop supports safe autocorrection (--autocorrect). -Layout/SpaceAfterMethodName: - Exclude: - - 'app/models/project.rb' - - 'app/models/user.rb' - - 'config/deakin.rb' - - 'config/no_institution_setting.rb' - -# Offense count: 14 +# Offense count: 3 # This cop supports safe autocorrection (--autocorrect). Layout/SpaceAfterNot: Exclude: - - 'app/api/group_sets_api.rb' - - 'app/api/tutorial_enrolments_api.rb' - - 'app/models/comments/extension_comment.rb' - - 'app/models/comments/task_comment.rb' - - 'app/models/group.rb' - - 'app/models/project.rb' - - 'app/models/task.rb' - - 'app/models/tutorial.rb' - - 'app/models/tutorial_enrolment.rb' - 'app/models/unit.rb' - - 'config/deakin.rb' - -# Offense count: 22 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyleInsidePipes. -# SupportedStylesInsidePipes: space, no_space -Layout/SpaceAroundBlockParameters: - Exclude: - - 'app/api/entities/project_entity.rb' - - 'app/models/task.rb' - - 'app/models/webcal.rb' - - 'lib/helpers/database_populator.rb' - - 'lib/tasks/init.rake' - -# Offense count: 6 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: space, no_space -Layout/SpaceAroundEqualsInParameterDefault: - Exclude: - - 'app/models/teaching_period.rb' - - 'app/models/unit.rb' - - 'lib/helpers/faker_randomiser.rb' -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Layout/SpaceAroundMethodCallOperator: - Exclude: - - 'app/models/unit.rb' - -# Offense count: 7 +# Offense count: 2 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowForAlignment, EnforcedStyleForExponentOperator. +# Configuration parameters: AllowForAlignment, EnforcedStyleForExponentOperator, EnforcedStyleForRationalLiterals. # SupportedStylesForExponentOperator: space, no_space +# SupportedStylesForRationalLiterals: space, no_space Layout/SpaceAroundOperators: Exclude: - - 'app/api/submission/portfolio_evidence_api.rb' - - 'app/models/task.rb' - - 'config/deakin.rb' - - 'config/initializers/swagger.rb' - 'lib/helpers/database_populator.rb' -# Offense count: 12 +# Offense count: 2 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces. # SupportedStyles: space, no_space @@ -479,170 +133,39 @@ Layout/SpaceAroundOperators: Layout/SpaceBeforeBlockBraces: Exclude: - 'app/helpers/file_helper.rb' - - 'app/models/activity_type.rb' - - 'app/models/campus.rb' - - 'app/models/project.rb' - 'app/models/task_definition.rb' - - 'app/models/teaching_period.rb' - - 'app/models/unit.rb' - - 'app/models/webcal.rb' -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Layout/SpaceBeforeBrackets: - Exclude: - - 'app/models/unit.rb' - -# Offense count: 118 +# Offense count: 2 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBrackets. # SupportedStyles: space, no_space, compact # SupportedStylesForEmptyBrackets: space, no_space Layout/SpaceInsideArrayLiteralBrackets: Exclude: - - 'app/api/projects_api.rb' - - 'app/api/units_api.rb' - - 'app/api/users_api.rb' - 'app/helpers/file_helper.rb' - - 'app/models/group.rb' - - 'app/models/group_set.rb' - - 'app/models/project.rb' - - 'app/models/task.rb' - - 'app/models/task_definition.rb' - - 'app/models/unit.rb' - - 'app/models/unit_role.rb' - - 'app/models/user.rb' - - 'config/deakin.rb' - - 'config/initializers/devise.rb' - - 'lib/helpers/database_populator.rb' -# Offense count: 34 +# Offense count: 5 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters. # SupportedStyles: space, no_space # SupportedStylesForEmptyBraces: space, no_space Layout/SpaceInsideBlockBraces: Exclude: - - 'app/api/entities/project_entity.rb' - - 'app/helpers/file_helper.rb' - - 'app/mailers/notifications_mailer.rb' - - 'app/models/activity_type.rb' - - 'app/models/campus.rb' - - 'app/models/portfolio_evidence.rb' - - 'app/models/project.rb' - - 'app/models/task.rb' - - 'app/models/task_definition.rb' - - 'app/models/teaching_period.rb' - - 'app/models/unit.rb' - - 'app/models/webcal.rb' - -# Offense count: 69 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces. -# SupportedStyles: space, no_space, compact -# SupportedStylesForEmptyBraces: space, no_space -Layout/SpaceInsideHashLiteralBraces: - Exclude: - - 'app/api/api_root.rb' - - 'app/api/extension_comments_api.rb' - - 'app/api/group_sets_api.rb' - - 'app/api/projects_api.rb' - - 'app/api/task_comments_api.rb' - - 'app/api/teaching_periods_authenticated_api.rb' - - 'app/api/units_api.rb' - - 'app/models/overseer_assessment.rb' - - 'app/models/project.rb' - - 'app/models/tutorial_stream.rb' - - 'app/models/unit.rb' - - 'app/models/user.rb' - - 'config/deakin.rb' - - 'lib/helpers/database_populator.rb' - -# Offense count: 27 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: space, compact, no_space -Layout/SpaceInsideParens: - Exclude: - - 'app/api/api_root.rb' - - 'app/api/units_api.rb' - 'app/helpers/file_helper.rb' - - 'app/models/project.rb' - - 'app/models/task_definition.rb' - - 'app/models/tutorial_enrolment.rb' - - 'app/models/unit.rb' - - 'config/deakin.rb' - - 'lib/helpers/database_populator.rb' - - 'lib/tasks/generate_pdfs.rake' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Layout/SpaceInsideRangeLiteral: - Exclude: - - 'lib/helpers/database_populator.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBrackets. -# SupportedStyles: space, no_space -# SupportedStylesForEmptyBrackets: space, no_space -Layout/SpaceInsideReferenceBrackets: - Exclude: - - 'app/models/unit.rb' - -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: space, no_space -Layout/SpaceInsideStringInterpolation: - Exclude: - - 'app/helpers/file_helper.rb' - - 'app/models/tutorial_enrolment.rb' - -# Offense count: 4 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: final_newline, final_blank_line -Layout/TrailingEmptyLines: - Exclude: - - 'app/channels/application_cable/channel.rb' - - 'config/initializers/swagger.rb' - - 'lib/helpers/faker_randomiser.rb' - - 'lib/tasks/send_status_emails.rake' - -# Offense count: 10 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowInHeredoc. -Layout/TrailingWhitespace: - Exclude: - - 'app/mailers/convenor_contact_mailer.rb' - - 'app/mailers/portfolio_evidence_mailer.rb' - - 'app/models/portfolio_evidence.rb' - - 'config/deakin.rb' - - 'lib/tasks/send_status_emails.rake' + - 'app/models/task_definition.rb' # Offense count: 1 -# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods. +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowedMethods, AllowedPatterns. Lint/AmbiguousBlockAssociation: Exclude: - 'app/models/task.rb' -# Offense count: 3 +# Offense count: 1 # This cop supports safe autocorrection (--autocorrect). Lint/AmbiguousOperator: Exclude: - - 'app/helpers/file_helper.rb' - 'app/models/portfolio_evidence.rb' - - 'app/models/task.rb' - -# Offense count: 14 -# This cop supports safe autocorrection (--autocorrect). -Lint/AmbiguousOperatorPrecedence: - Exclude: - - 'app/models/project.rb' - - 'app/models/task.rb' - - 'app/models/unit.rb' - - 'lib/tasks/populate.rake' # Offense count: 7 # This cop supports safe autocorrection (--autocorrect). @@ -655,28 +178,12 @@ Lint/AmbiguousRegexpLiteral: - 'app/helpers/file_helper.rb' # Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). +# This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: AllowSafeAssignment. Lint/AssignmentInCondition: Exclude: - 'app/channels/application_cable/connection.rb' -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -Lint/DeprecatedClassMethods: - Exclude: - - 'app/models/task.rb' - -# Offense count: 24 -# Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches. -Lint/DuplicateBranch: - Exclude: - - 'app/api/api_root.rb' - - 'app/helpers/file_helper.rb' - - 'app/models/project.rb' - - 'app/models/task_status.rb' - - 'lib/tasks/populate.rake' - # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). Lint/ElseLayout: @@ -684,21 +191,15 @@ Lint/ElseLayout: - 'app/models/project.rb' - 'app/models/task.rb' -# Offense count: 1 -# Configuration parameters: AllowComments, AllowEmptyLambdas. -Lint/EmptyBlock: - Exclude: - - 'app/helpers/file_helper.rb' - # Offense count: 1 Lint/FloatComparison: Exclude: - 'app/models/project.rb' -# Offense count: 8 +# Offense count: 12 +# This cop supports safe autocorrection (--autocorrect). Lint/ImplicitStringConcatenation: Exclude: - - 'app/api/learning_alignment_api.rb' - 'app/api/learning_outcomes_api.rb' # Offense count: 2 @@ -709,6 +210,7 @@ Lint/Loop: - 'config/no_institution_setting.rb' # Offense count: 4 +# Configuration parameters: AllowedParentClasses. Lint/MissingSuper: Exclude: - 'app/controllers/lecture_resource_downloads_controller.rb' @@ -716,15 +218,6 @@ Lint/MissingSuper: - 'app/controllers/task_downloads_controller.rb' - 'app/controllers/task_submission_pdfs_controller.rb' -# Offense count: 18 -# This cop supports unsafe autocorrection (--autocorrect-all). -Lint/NonAtomicFileOperation: - Exclude: - - 'app/helpers/file_helper.rb' - - 'app/models/comments/task_comment.rb' - - 'app/models/project.rb' - - 'app/models/task.rb' - # Offense count: 1 Lint/NonLocalExitFromIterator: Exclude: @@ -736,18 +229,13 @@ Lint/ParenthesesAsGroupedExpression: Exclude: - 'app/models/unit.rb' -# Offense count: 5 +# Offense count: 4 # This cop supports safe autocorrection (--autocorrect). Lint/RedundantStringCoercion: Exclude: - 'app/helpers/file_helper.rb' -# Offense count: 2 -Lint/RequireParentheses: - Exclude: - - 'config/application.rb' - -# Offense count: 15 +# Offense count: 16 Lint/RescueException: Exclude: - 'app/models/portfolio_evidence.rb' @@ -756,34 +244,16 @@ Lint/RescueException: - 'config/deakin.rb' - 'lib/tasks/generate_pdfs.rake' -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Lint/ScriptPermission: - Exclude: - - 'Rakefile' - -# Offense count: 8 +# Offense count: 4 Lint/ShadowingOuterLocalVariable: Exclude: - - 'app/models/learning_outcome.rb' - - 'app/models/teaching_period.rb' - 'app/models/unit.rb' -# Offense count: 4 +# Offense count: 1 # Configuration parameters: AllowComments, AllowNil. Lint/SuppressedException: Exclude: - - 'app/models/project.rb' - 'app/models/task.rb' - - 'app/models/task_definition.rb' - -# Offense count: 5 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: strict, consistent -Lint/SymbolConversion: - Exclude: - - 'lib/tasks/init.rake' # Offense count: 2 # Configuration parameters: AllowKeywordBlockArguments. @@ -791,9 +261,9 @@ Lint/UnderscorePrefixedVariableName: Exclude: - 'app/models/comments/discussion_comment.rb' -# Offense count: 33 +# Offense count: 45 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. +# Configuration parameters: AutoCorrect, IgnoreEmptyBlocks, AllowUnusedKeywordArguments. Lint/UnusedBlockArgument: Exclude: - 'app/api/entities/comment_entity.rb' @@ -811,9 +281,10 @@ Lint/UnusedBlockArgument: - 'lib/helpers/database_populator.rb' - 'lib/tasks/checks.rake' -# Offense count: 7 +# Offense count: 9 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods. +# Configuration parameters: AutoCorrect, AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions. +# NotImplementedExceptions: NotImplementedError Lint/UnusedMethodArgument: Exclude: - 'app/models/project.rb' @@ -821,51 +292,53 @@ Lint/UnusedMethodArgument: - 'config/deakin.rb' - 'config/no_institution_setting.rb' -# Offense count: 55 +# Offense count: 61 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AutoCorrect. Lint/UselessAssignment: Enabled: false -# Offense count: 145 -# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods, CountRepeatedAttributes. +# Offense count: 225 +# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: Max: 153 -# Offense count: 65 -# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, AllowedMethods, AllowedPatterns, IgnoredMethods. +# Offense count: 96 +# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. # AllowedMethods: refine Metrics/BlockLength: - Max: 200 + Max: 157 -# Offense count: 12 -# Configuration parameters: CountBlocks. +# Offense count: 10 +# Configuration parameters: CountBlocks, CountModifierForms. Metrics/BlockNesting: Max: 5 -# Offense count: 65 -# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods. +# Offense count: 96 +# Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/CyclomaticComplexity: - Max: 39 + Max: 36 -# Offense count: 173 -# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, AllowedMethods, AllowedPatterns, IgnoredMethods. +# Offense count: 273 +# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: - Max: 140 + Max: 115 -# Offense count: 1 +# Offense count: 4 # Configuration parameters: CountComments, CountAsOne. Metrics/ModuleLength: - Enabled: false + Max: 580 -# Offense count: 4 +# Offense count: 7 # Configuration parameters: CountKeywordArgs. Metrics/ParameterLists: MaxOptionalParameters: 4 Max: 8 -# Offense count: 62 -# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods. +# Offense count: 84 +# Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/PerceivedComplexity: - Max: 50 + Max: 39 # Offense count: 2 Naming/AccessorMethodName: @@ -874,13 +347,14 @@ Naming/AccessorMethodName: - 'app/models/user.rb' # Offense count: 1 -# Configuration parameters: EnforcedStyle, AllowedPatterns, IgnoredPatterns. +# Configuration parameters: EnforcedStyle, AllowedPatterns, ForbiddenIdentifiers, ForbiddenPatterns. # SupportedStyles: snake_case, camelCase +# ForbiddenIdentifiers: __id__, __send__ Naming/MethodName: Exclude: - 'app/models/comments/discussion_comment.rb' -# Offense count: 16 +# Offense count: 13 # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. # AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to Naming/MethodParameterName: @@ -892,15 +366,14 @@ Naming/MethodParameterName: - 'app/models/task.rb' - 'app/models/unit.rb' -# Offense count: 36 -# Configuration parameters: NamePrefix, ForbiddenPrefixes, AllowedMethods, MethodDefinitionMacros. -# NamePrefix: is_, has_, have_ -# ForbiddenPrefixes: is_, has_, have_ +# Offense count: 37 +# Configuration parameters: NamePrefix, ForbiddenPrefixes, AllowedMethods, MethodDefinitionMacros, UseSorbetSigs. +# NamePrefix: is_, has_, have_, does_ +# ForbiddenPrefixes: is_, has_, have_, does_ # AllowedMethods: is_a? # MethodDefinitionMacros: define_method, define_singleton_method Naming/PredicateName: Exclude: - - 'spec/**/*' - 'app/api/entities/unit_entity.rb' - 'app/models/group.rb' - 'app/models/overseer_assessment.rb' @@ -914,7 +387,7 @@ Naming/PredicateName: - 'lib/tasks/generate_pdfs.rake' # Offense count: 27 -# Configuration parameters: EnforcedStyle, AllowedIdentifiers, AllowedPatterns. +# Configuration parameters: EnforcedStyle, AllowedIdentifiers, AllowedPatterns, ForbiddenIdentifiers, ForbiddenPatterns. # SupportedStyles: snake_case, camelCase Naming/VariableName: Exclude: @@ -922,28 +395,12 @@ Naming/VariableName: - 'app/models/comments/task_comment.rb' - 'config/deakin.rb' -# Offense count: 2 -# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns. -# SupportedStyles: snake_case, normalcase, non_integer -# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339 -Naming/VariableNumber: - Exclude: - - 'app/models/unit.rb' - -# Offense count: 3 -# This cop supports unsafe autocorrection (--autocorrect-all). -Security/IoMethods: - Exclude: - - 'app/api/discussion_comment_api.rb' - - 'app/api/task_comments_api.rb' - -# Offense count: 13 +# Offense count: 5 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: separated, grouped Style/AccessorGrouping: Exclude: - - 'app/models/project.rb' - 'app/models/task.rb' # Offense count: 2 @@ -970,9 +427,9 @@ Style/AndOr: - 'app/models/tutorial_enrolment.rb' - 'app/models/tutorial_stream.rb' -# Offense count: 7 +# Offense count: 6 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, ProceduralMethods, FunctionalMethods, AllowedMethods, AllowedPatterns, IgnoredMethods, AllowBracesOnProceduralOneLiners, BracesRequiredMethods. +# Configuration parameters: EnforcedStyle, ProceduralMethods, FunctionalMethods, AllowedMethods, AllowedPatterns, AllowBracesOnProceduralOneLiners, BracesRequiredMethods. # SupportedStyles: line_count_based, semantic, braces_for_chaining, always_braces # ProceduralMethods: benchmark, bm, bmbm, create, each_with_object, measure, new, realtime, tap, with_object # FunctionalMethods: let, let!, subject, watch @@ -986,28 +443,26 @@ Style/BlockDelimiters: - 'config/deakin.rb' - 'lib/helpers/database_populator.rb' -# Offense count: 6 +# Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: MinBranchesCount. Style/CaseLikeIf: Exclude: - 'app/helpers/csv_helper.rb' - - 'app/helpers/file_helper.rb' - - 'app/models/comments/task_comment.rb' - - 'app/models/user.rb' -# Offense count: 3 +# Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle. +# Configuration parameters: EnforcedStyle, EnforcedStyleForClasses, EnforcedStyleForModules. # SupportedStyles: nested, compact +# SupportedStylesForClasses: , nested, compact +# SupportedStylesForModules: , nested, compact Style/ClassAndModuleChildren: Exclude: - - 'app/api/entities/minimal/minimal_unit_entity.rb' - - 'app/api/entities/minimal/minimal_user_entity.rb' - 'app/api/submission/generate_helpers.rb' -# Offense count: 12 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods. +# Offense count: 4 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: AllowedMethods, AllowedPatterns. # AllowedMethods: ==, equal?, eql? Style/ClassEqualityComparison: Exclude: @@ -1022,6 +477,7 @@ Style/ColonMethodCall: - 'app/helpers/timeout_helper.rb' # Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). Style/CombinableLoops: Exclude: - 'app/models/unit.rb' @@ -1034,18 +490,17 @@ Style/CommentAnnotation: Exclude: - 'app/api/task_definitions_api.rb' -# Offense count: 24 +# Offense count: 20 # This cop supports unsafe autocorrection (--autocorrect-all). Style/CommentedKeyword: Exclude: - 'app/api/projects_api.rb' - - 'app/api/submission/batch_task_api.rb' - 'app/api/submission/portfolio_api.rb' - 'app/api/submission/portfolio_evidence_api.rb' - 'app/models/unit.rb' - 'config/deakin.rb' -# Offense count: 13 +# Offense count: 12 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions. # SupportedStyles: assign_to_condition, assign_inside_condition @@ -1053,30 +508,28 @@ Style/ConditionalAssignment: Exclude: - 'app/api/submission/portfolio_evidence_api.rb' - 'app/models/comments/extension_comment.rb' - - 'app/models/learning_outcome_task_link.rb' - 'app/models/task.rb' - 'app/models/unit.rb' - 'lib/helpers/database_populator.rb' - 'lib/tasks/maintenance.rake' -# Offense count: 11 +# Offense count: 9 # This cop supports safe autocorrection (--autocorrect). Style/DefWithParentheses: Exclude: - 'app/helpers/file_helper.rb' - - 'app/models/overseer_assessment.rb' - 'app/models/task_definition.rb' - 'app/models/user.rb' - 'config/deakin.rb' -# Offense count: 120 +# Offense count: 197 # Configuration parameters: AllowedConstants. Style/Documentation: Enabled: false # Offense count: 3 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, AllowComments. +# Configuration parameters: AutoCorrect, EnforcedStyle, AllowComments. # SupportedStyles: empty, nil, both Style/EmptyElse: Exclude: @@ -1105,30 +558,7 @@ Style/ExplicitBlockArgument: Exclude: - 'app/helpers/timeout_helper.rb' -# Offense count: 31 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowedVars. -Style/FetchEnvVar: - Exclude: - - 'config/application.rb' - - 'config/deakin.rb' - - 'config/environments/production.rb' - -# Offense count: 3 -# This cop supports safe autocorrection (--autocorrect). -Style/FileRead: - Exclude: - - 'app/helpers/file_helper.rb' - - 'app/models/unit.rb' - - 'lib/tasks/checks.rake' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Style/FileWrite: - Exclude: - - 'lib/tasks/generate_pdfs.rake' - -# Offense count: 19 +# Offense count: 18 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: each, for @@ -1153,27 +583,28 @@ Style/FormatString: Exclude: - 'app/models/project.rb' -# Offense count: 8 +# Offense count: 7 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: MaxUnannotatedPlaceholdersAllowed, AllowedMethods, AllowedPatterns, IgnoredMethods. +# Configuration parameters: MaxUnannotatedPlaceholdersAllowed, Mode, AllowedMethods, AllowedPatterns. # SupportedStyles: annotated, template, unannotated +# AllowedMethods: redirect Style/FormatStringToken: EnforcedStyle: template -# Offense count: 152 +# Offense count: 251 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: always, always_true, never Style/FrozenStringLiteralComment: Enabled: false -# Offense count: 2 +# Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). Style/GlobalStdStream: Exclude: - 'lib/tasks/skip_prod.rake' -# Offense count: 46 +# Offense count: 70 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. Style/GuardClause: @@ -1183,7 +614,7 @@ Style/GuardClause: # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. # SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys -# SupportedShorthandSyntax: always, never, either, consistent +# SupportedShorthandSyntax: always, never, either, consistent, either_consistent Style/HashSyntax: Exclude: - 'app/models/campus.rb' @@ -1197,30 +628,20 @@ Style/HashSyntax: Style/IfInsideElse: Exclude: - 'app/api/units_api.rb' - - 'app/models/learning_outcome_task_link.rb' - 'app/models/task.rb' -# Offense count: 316 +# Offense count: 500 # This cop supports safe autocorrection (--autocorrect). Style/IfUnlessModifier: Enabled: false -# Offense count: 2 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AllowedMethods. -# AllowedMethods: nonzero? -Style/IfWithBooleanLiteralBranches: - Exclude: - - 'config/application.rb' - -# Offense count: 5 +# Offense count: 3 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: InverseMethods, InverseBlocks. Style/InverseMethods: Exclude: - 'app/api/entities/task_entity.rb' - 'app/api/submission/generate_helpers.rb' - - 'app/helpers/file_helper.rb' - 'app/models/unit.rb' # Offense count: 15 @@ -1229,29 +650,26 @@ Style/InverseMethods: # SupportedStyles: line_count_dependent, lambda, literal Style/Lambda: Exclude: - - 'app/api/entities/project_entity.rb' - 'app/api/entities/unit_entity.rb' - 'app/models/project.rb' - 'app/models/unit.rb' - 'config/deakin.rb' -# Offense count: 25 +# Offense count: 27 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods. +# Configuration parameters: AllowedMethods, AllowedPatterns. Style/MethodCallWithoutArgsParentheses: Exclude: - 'app/api/task_definitions_api.rb' - 'app/models/comments/discussion_comment.rb' - - 'app/models/overseer_assessment.rb' - 'app/models/project.rb' - - 'app/models/task.rb' - 'app/models/task_definition.rb' - 'app/models/unit.rb' - 'config/deakin.rb' - 'lib/helpers/database_populator.rb' - 'lib/tasks/checks.rake' -# Offense count: 12 +# Offense count: 10 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: require_parentheses, require_no_parentheses, require_no_parentheses_except_multiline @@ -1262,7 +680,6 @@ Style/MethodDefParentheses: - 'app/models/group_submission.rb' - 'app/models/task_definition.rb' - 'config/deakin.rb' - - 'lib/helpers/database_populator.rb' # Offense count: 1 Style/MultilineBlockChain: @@ -1270,14 +687,12 @@ Style/MultilineBlockChain: - 'app/models/project.rb' # Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: literals, strict -Style/MutableConstant: +# This cop supports safe autocorrection (--autocorrect). +Style/MultilineIfModifier: Exclude: - - 'app/helpers/grade_helper.rb' + - 'app/api/staff_grant_extension_api.rb' -# Offense count: 6 +# Offense count: 4 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: both, prefix, postfix @@ -1285,16 +700,7 @@ Style/NegatedIf: Exclude: - 'app/api/task_definitions_api.rb' - 'app/api/units_api.rb' - - 'app/helpers/file_helper.rb' - 'app/models/task.rb' - - 'app/models/task_definition.rb' - -# Offense count: 3 -# This cop supports safe autocorrection (--autocorrect). -Style/NegatedIfElseCondition: - Exclude: - - 'app/models/project.rb' - - 'app/models/unit.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). @@ -1302,14 +708,13 @@ Style/NestedTernaryOperator: Exclude: - 'app/models/project.rb' -# Offense count: 7 +# Offense count: 6 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, MinBodyLength. +# Configuration parameters: EnforcedStyle, MinBodyLength, AllowConsecutiveConditionals. # SupportedStyles: skip_modifier_ifs, always Style/Next: Exclude: - 'app/models/teaching_period.rb' - - 'app/models/unit.rb' - 'config/deakin.rb' # Offense count: 2 @@ -1335,28 +740,27 @@ Style/Not: Style/NumericLiterals: MinDigits: 6 -# Offense count: 90 +# Offense count: 97 # This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle, AllowedMethods, AllowedPatterns, IgnoredMethods. +# Configuration parameters: EnforcedStyle, AllowedMethods, AllowedPatterns. # SupportedStyles: predicate, comparison Style/NumericPredicate: Enabled: false -# Offense count: 23 +# Offense count: 27 # Configuration parameters: AllowedMethods. # AllowedMethods: respond_to_missing? Style/OptionalBooleanParameter: Exclude: - 'app/helpers/file_helper.rb' + - 'app/mailers/notifications_mailer.rb' - 'app/models/auth_token.rb' - 'app/models/comments/extension_comment.rb' - 'app/models/portfolio_evidence.rb' - - 'app/models/project.rb' - 'app/models/task.rb' - 'app/models/task_definition.rb' - - 'app/models/teaching_period.rb' - 'app/models/unit.rb' - - 'app/models/user.rb' + - 'app/services/extension_service.rb' - 'lib/helpers/faker_randomiser.rb' # Offense count: 2 @@ -1365,16 +769,14 @@ Style/OrAssignment: Exclude: - 'app/models/unit.rb' -# Offense count: 4 +# Offense count: 1 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowSafeAssignment, AllowInMultilineConditions. Style/ParenthesesAroundCondition: Exclude: - - 'app/models/group.rb' - - 'app/models/task_definition.rb' - 'config/application.rb' -# Offense count: 29 +# Offense count: 34 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: PreferredDelimiters. Style/PercentLiteralDelimiters: @@ -1382,7 +784,6 @@ Style/PercentLiteralDelimiters: - 'app/api/task_comments_api.rb' - 'app/helpers/file_helper.rb' - 'app/models/learning_outcome.rb' - - 'app/models/learning_outcome_task_link.rb' - 'app/models/task.rb' - 'app/models/task_definition.rb' - 'app/models/unit.rb' @@ -1390,36 +791,20 @@ Style/PercentLiteralDelimiters: - 'app/models/webcal.rb' - 'config/application.rb' -# Offense count: 12 +# Offense count: 16 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: . # SupportedStyles: same_as_string_literals, single_quotes, double_quotes Style/QuotedSymbols: EnforcedStyle: double_quotes -# Offense count: 5 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: Methods. -Style/RedundantArgument: - Exclude: - - 'app/helpers/csv_helper.rb' - - 'app/models/unit.rb' - - 'app/models/user.rb' - -# Offense count: 5 +# Offense count: 3 # This cop supports safe autocorrection (--autocorrect). Style/RedundantBegin: Exclude: - 'app/helpers/timeout_helper.rb' - - 'app/models/task_definition.rb' - 'app/models/unit.rb' -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantConstantBase: - Exclude: - - 'config.ru' - # Offense count: 2 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: SafeForConstants. @@ -1427,13 +812,7 @@ Style/RedundantFetchBlock: Exclude: - 'config/puma.rb' -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantFileExtensionInRequire: - Exclude: - - 'lib/tasks/register_q_assessment_results_subscriber.rake' - -# Offense count: 11 +# Offense count: 16 # This cop supports unsafe autocorrection (--autocorrect-all). Style/RedundantInterpolation: Exclude: @@ -1441,26 +820,21 @@ Style/RedundantInterpolation: - 'app/models/task_definition.rb' - 'lib/helpers/database_populator.rb' -# Offense count: 6 +# Offense count: 3 # This cop supports safe autocorrection (--autocorrect). Style/RedundantParentheses: Exclude: - 'app/models/project.rb' - 'app/models/task.rb' - - 'app/models/task_definition.rb' - 'config/application.rb' -# Offense count: 17 +# Offense count: 8 # This cop supports safe autocorrection (--autocorrect). Style/RedundantRegexpEscape: Exclude: - - 'app/api/discussion_comment_api.rb' - - 'app/api/task_comments_api.rb' - 'app/helpers/csv_helper.rb' - - 'app/helpers/file_helper.rb' - - 'app/models/project.rb' -# Offense count: 20 +# Offense count: 21 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowMultipleReturnValues. Style/RedundantReturn: @@ -1474,7 +848,7 @@ Style/RedundantReturn: - 'app/models/user.rb' - 'app/models/webcal.rb' -# Offense count: 88 +# Offense count: 174 # This cop supports safe autocorrection (--autocorrect). Style/RedundantSelf: Enabled: false @@ -1485,13 +859,7 @@ Style/RedundantSort: Exclude: - 'app/models/project.rb' -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantStringEscape: - Exclude: - - 'app/api/authentication_api.rb' - -# Offense count: 13 +# Offense count: 9 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, AllowInnerSlashes. # SupportedStyles: slashes, percent_r, mixed @@ -1503,10 +871,7 @@ Style/RegexpLiteral: - 'app/controllers/task_submission_pdfs_controller.rb' - 'app/helpers/csv_helper.rb' - 'app/helpers/file_helper.rb' - - 'app/models/project.rb' - 'app/models/task.rb' - - 'app/models/task_definition.rb' - - 'app/models/unit.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). @@ -1514,14 +879,16 @@ Style/RescueModifier: Exclude: - 'app/models/unit.rb' -# Offense count: 27 +# Offense count: 22 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: implicit, explicit Style/RescueStandardError: Exclude: + - 'app/api/staff_grant_extension_api.rb' - 'app/helpers/file_helper.rb' - 'app/helpers/timeout_helper.rb' + - 'app/mailers/notifications_mailer.rb' - 'app/models/group_submission.rb' - 'app/models/portfolio_evidence.rb' - 'app/models/project.rb' @@ -1532,13 +899,12 @@ Style/RescueStandardError: - 'lib/tasks/checks.rake' - 'lib/tasks/maintenance.rake' -# Offense count: 29 +# Offense count: 41 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. # AllowedMethods: present?, blank?, presence, try, try! Style/SafeNavigation: Exclude: - - 'app/api/entities/minimal/minimal_unit_entity.rb' - 'app/api/entities/task_definition_entity.rb' - 'app/api/entities/tutorial_entity.rb' - 'app/api/entities/unit_entity.rb' @@ -1548,13 +914,6 @@ Style/SafeNavigation: - 'app/models/tutorial.rb' - 'app/models/unit.rb' - 'app/models/user.rb' - - 'lib/assets/ontrack_receive_action.rb' - -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/SelectByRegexp: - Exclude: - - 'app/helpers/file_helper.rb' # Offense count: 3 # This cop supports safe autocorrection (--autocorrect). @@ -1562,30 +921,29 @@ Style/SelfAssignment: Exclude: - 'app/models/unit.rb' -# Offense count: 4 +# Offense count: 2 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowAsExpressionSeparator. Style/Semicolon: Exclude: - - 'app/models/unit.rb' - 'lib/helpers/database_populator.rb' - 'lib/tasks/generate_pdfs.rake' -# Offense count: 2 +# Offense count: 3 # This cop supports unsafe autocorrection (--autocorrect-all). Style/SlicingWithRange: Exclude: - 'app/models/task.rb' -# Offense count: 8 +# Offense count: 9 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowModifier. Style/SoleNestedConditional: Exclude: - 'app/api/group_sets_api.rb' - - 'app/api/task_definitions_api.rb' - 'app/models/group.rb' - 'app/models/task.rb' + - 'app/services/extension_service.rb' - 'config/deakin.rb' # Offense count: 10 @@ -1597,7 +955,7 @@ Style/StringConcatenation: - 'app/models/task.rb' - 'app/models/unit.rb' -# Offense count: 349 +# Offense count: 627 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. # SupportedStyles: single_quotes, double_quotes @@ -1615,20 +973,19 @@ Style/StringLiteralsInInterpolation: - 'config/deakin.rb' - 'lib/helpers/database_populator.rb' -# Offense count: 41 +# Offense count: 72 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, MinSize. # SupportedStyles: percent, brackets Style/SymbolArray: Enabled: false -# Offense count: 5 +# Offense count: 4 # This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AllowMethodsWithArguments, AllowedMethods, AllowedPatterns, IgnoredMethods, AllowComments. -# AllowedMethods: define_method +# Configuration parameters: AllowMethodsWithArguments, AllowedMethods, AllowedPatterns, AllowComments. +# AllowedMethods: define_method, mail, respond_to Style/SymbolProc: Exclude: - - 'app/models/teaching_period.rb' - 'app/models/unit.rb' # Offense count: 2 @@ -1640,7 +997,7 @@ Style/TernaryParentheses: - 'app/models/project.rb' - 'app/models/tutorial_stream.rb' -# Offense count: 5 +# Offense count: 7 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyleForMultiline. # SupportedStylesForMultiline: comma, consistent_comma, no_comma @@ -1650,20 +1007,20 @@ Style/TrailingCommaInArguments: - 'app/api/units_api.rb' - 'app/models/unit.rb' -# Offense count: 6 +# Offense count: 9 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyleForMultiline. -# SupportedStylesForMultiline: comma, consistent_comma, no_comma +# SupportedStylesForMultiline: comma, consistent_comma, diff_comma, no_comma Style/TrailingCommaInArrayLiteral: Exclude: - 'app/models/task.rb' - 'app/models/unit.rb' - 'lib/helpers/database_populator.rb' -# Offense count: 7 +# Offense count: 10 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyleForMultiline. -# SupportedStylesForMultiline: comma, consistent_comma, no_comma +# SupportedStylesForMultiline: comma, consistent_comma, diff_comma, no_comma Style/TrailingCommaInHashLiteral: Exclude: - 'app/models/comments/task_comment.rb' @@ -1684,15 +1041,9 @@ Style/WordArray: - 'config/deakin.rb' - 'lib/helpers/database_populator.rb' -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/ZeroLengthPredicate: - Exclude: - - 'app/models/unit.rb' - -# Offense count: 583 +# Offense count: 594 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, IgnoredPatterns. +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings. # URISchemes: http, https Layout/LineLength: Max: 369 diff --git a/Gemfile.lock b/Gemfile.lock index 1149766c4..68cf502eb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -98,6 +98,7 @@ GEM bunny-pub-sub (0.5.2) bunny (~> 2.14) byebug (12.0.0) + cgi (0.4.2) chronic_duration (0.10.6) numerizer (~> 0.1.1) ci_reporter (2.1.0) @@ -152,6 +153,8 @@ GEM dry-inflector (~> 1.0) dry-logic (~> 1.4) zeitwerk (~> 2.6) + erb (4.0.4) + cgi (>= 0.3.3) erubi (1.13.1) erubis (2.7.0) et-orbi (1.2.11) @@ -173,7 +176,6 @@ GEM faraday (>= 1, < 3) faraday-net_http (3.4.0) net-http (>= 0.5.0) - ffi (1.17.1-aarch64-linux-gnu) ffi (1.17.1-x86_64-linux-gnu) fugit (1.11.1) et-orbi (~> 1, >= 1.2.11) @@ -208,7 +210,7 @@ GEM ice_cube (~> 0.16) ostruct ice_cube (0.17.0) - io-console (0.8.0) + io-console (0.8.1) irb (1.15.1) pp (>= 0.6.0) rdoc (>= 4.0.0) @@ -279,8 +281,6 @@ GEM net-protocol netrc (0.11.0) nio4r (2.7.4) - nokogiri (1.18.7-aarch64-linux-gnu) - racc (~> 1.4) nokogiri (1.18.7-x86_64-linux-gnu) racc (~> 1.4) numerizer (0.1.1) @@ -308,7 +308,7 @@ GEM pp (0.6.2) prettyprint prettyprint (0.2.0) - prism (1.4.0) + prism (1.5.1) psych (5.2.3) date stringio @@ -374,7 +374,8 @@ GEM rbs (3.9.2) logger rbtree (0.4.6) - rdoc (6.13.1) + rdoc (6.14.0) + erb psych (>= 4.0.0) redis (5.4.0) redis-client (>= 0.22.0) @@ -537,7 +538,7 @@ GEM unicode-display_width (3.1.4) unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) - uri (1.0.3) + uri (1.0.4) useragent (0.16.11) version_gem (1.1.6) warden (1.2.9) @@ -556,8 +557,7 @@ GEM zeitwerk (2.7.2) PLATFORMS - aarch64-linux - x86_64-linux + x86_64-linux-gnu DEPENDENCIES better_errors @@ -623,7 +623,7 @@ DEPENDENCIES webmock RUBY VERSION - ruby 3.4.2p28 + ruby 3.4.7p58 BUNDLED WITH - 2.6.6 + 2.6.9 diff --git a/Rakefile b/Rakefile index 350ebd498..f36b80f28 100644 --- a/Rakefile +++ b/Rakefile @@ -1,4 +1,3 @@ -#!/usr/bin/env rake # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. diff --git a/app/api/api_root.rb b/app/api/api_root.rb index 983749c55..42c8f4ff1 100644 --- a/app/api/api_root.rb +++ b/app/api/api_root.rb @@ -63,8 +63,12 @@ class ApiRoot < Grape::API mount ScormExtensionCommentsApi mount GroupSetsApi mount LearningOutcomesApi + # mount LearningAlignmentApi + # the mount above is available in 9.x but has not been ported to `10.0.x` + mount NotificationsApi mount ProjectsApi mount SettingsApi + mount StaffGrantExtensionApi mount StudentsApi mount Submission::PortfolioApi mount Submission::PortfolioEvidenceApi @@ -118,6 +122,7 @@ class ApiRoot < Grape::API AuthenticationHelpers.add_auth_to GroupSetsApi AuthenticationHelpers.add_auth_to LearningOutcomesApi AuthenticationHelpers.add_auth_to ProjectsApi + AuthenticationHelpers.add_auth_to StaffGrantExtensionApi AuthenticationHelpers.add_auth_to StudentsApi AuthenticationHelpers.add_auth_to Submission::PortfolioApi AuthenticationHelpers.add_auth_to Submission::PortfolioEvidenceApi diff --git a/app/api/extension_comments_api.rb b/app/api/extension_comments_api.rb index 1d510f079..966d69e6f 100644 --- a/app/api/extension_comments_api.rb +++ b/app/api/extension_comments_api.rb @@ -10,29 +10,21 @@ class ExtensionCommentsApi < Grape::API requires :weeks_requested, type: Integer, desc: 'The details of the request' end post '/projects/:project_id/task_def_id/:task_definition_id/request_extension' do - project = Project.find(params[:project_id]) - task_definition = project.unit.task_definitions.find(params[:task_definition_id]) - task = project.task_for_task_definition(task_definition) - - # check permissions using specific permission has with addition of request extension if allowed in unit - unless authorise? current_user, task, :request_extension, ->(role, perm_hash, other) { task.specific_permission_hash(role, perm_hash, other) } - error!({ error: 'Not authorised to request an extension for this task' }, 403) - end - - if project.unit.allow_flexible_dates - error!({ error: 'Extensions are disabled for this unit.' }, 403) + # Use the ExtensionService to handle the extension request + result = ExtensionService.grant_extension( + params[:project_id], + params[:task_definition_id], + current_user, + params[:weeks_requested], + params[:comment] + ) + + # Handle the service response + if result[:success] + present result[:result].serialize(current_user), Grape::Presenters::Presenter + else + error!({ error: result[:error] }, result[:status]) end - - error!({ error: 'Extension weeks can not be 0.' }, 403) if params[:weeks_requested] == 0 - - max_duration = task.weeks_can_extend - duration = params[:weeks_requested] - duration = max_duration unless params[:weeks_requested] <= max_duration - - error!({ error: 'Extensions cannot be granted beyond task deadline.' }, 403) if duration <= 0 - - result = task.apply_for_extension(current_user, params[:comment], duration) - present result.serialize(current_user), Grape::Presenters::Presenter end desc 'Assess an extension for a task' diff --git a/app/api/notifications_api.rb b/app/api/notifications_api.rb new file mode 100644 index 000000000..371b08c5b --- /dev/null +++ b/app/api/notifications_api.rb @@ -0,0 +1,29 @@ +class NotificationsApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers + + before do + authenticated? + end + + desc 'Get current user notifications' + get '/notifications' do + notifications = current_user.notifications.order(created_at: :desc) + # Return array of notifications as JSON (id and message only) + notifications.as_json(only: [:id, :message]) + end + + desc 'Delete user notification by id' + delete '/notifications/:id' do + notification = current_user.notifications.find_by(id: params[:id]) + error!({ error: 'Notification not found' }, 404) unless notification + notification.destroy + status 204 + end + + desc 'Delete all user notifications' + delete '/notifications' do + current_user.notifications.delete_all + status 204 + end +end diff --git a/app/api/staff_grant_extension_api.rb b/app/api/staff_grant_extension_api.rb new file mode 100644 index 000000000..0db191ecb --- /dev/null +++ b/app/api/staff_grant_extension_api.rb @@ -0,0 +1,178 @@ +require 'grape' + +# +# API endpoint for staff to grant extensions to multiple students at once +# +class StaffGrantExtensionApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers + helpers DbHelpers + + before do + authenticated? + unless current_user.has_tutor_capability? + error!( + { + error: 'Not authorized to grant extensions', + code: 'UNAUTHORIZED', + details: {} + }, + 403 + ) + end + end + + desc 'Grant extensions to multiple students', + detail: 'This endpoint allows staff to grant extensions to multiple students at once for a specific task. The operation is atomic - either all extensions are granted or none are. Students not found in the unit are automatically skipped without affecting the transaction.', + success: [ + { code: 201, message: 'Extensions granted successfully' } + ], + failure: [ + { code: 400, message: 'Some extensions failed to be granted' }, + { code: 403, message: 'Not authorized to grant extensions for this unit' }, + { code: 404, message: 'Unit or task definition not found' }, + { code: 500, message: 'Internal server error' } + ], + response: { + successful: [ + { + student_id: 'Integer - ID of the student', + project_id: 'Integer - ID of the project', + weeks_requested: 'Integer - Number of weeks extension granted', + extension_response: 'String - Human readable message with new due date', + task_status: 'String - Updated status of the task' + } + ], + failed: [ + { + student_id: 'Integer - ID of the student', + project_id: 'Integer - ID of the project', + error: 'String - Error message explaining why extension failed' + } + ], + skipped: [ + { + student_id: 'Integer - ID of the student', + reason: 'String - Reason why the student was skipped' + } + ] + } + params do + requires :student_ids, type: Array[Integer], desc: 'List of student IDs to grant extensions to' + requires :task_definition_id, type: Integer, desc: 'Task definition ID' + requires :weeks_requested, type: Integer, desc: 'Number of weeks to extend by (1-4)' + requires :comment, type: String, desc: 'Reason for extension (max 300 characters)' + end + post '/units/:unit_id/staff-grant-extension' do + unit = Unit.find(params[:unit_id]) + task_definition = unit.task_definitions.find(params[:task_definition_id]) + + # Use transaction to ensure atomic operation + ActiveRecord::Base.transaction do + results = { + successful: [], + failed: [], + skipped: [] + } + + params[:student_ids].each do |student_id| + # Find project for this student in the unit + project = unit.projects.find_by(user_id: student_id) + if project.nil? + results[:skipped] << { + student_id: student_id, + reason: 'Student not found in unit' + } + next + end + result = ExtensionService.grant_extension( + project.id, + task_definition.id, + current_user, + params[:weeks_requested], + params[:comment], + is_staff_grant: true + ) + if result[:success] + extension_comment = result[:result] + results[:successful] << { + student_id: student_id, + project_id: project.id, + weeks_requested: extension_comment.extension_weeks, + extension_response: extension_comment.extension_response, + task_status: extension_comment.task.status, + extension_comment: extension_comment # Store internally for notifications + } + else + results[:failed] << { + student_id: student_id, + project_id: project.id, + error: result[:error] + } + # If it's a validation error (403), raise it immediately + error!({ error: result[:error] }, result[:status]) if result[:status] == 403 + end + end + + # If any extensions failed (but not due to validation), rollback the entire transaction + if results[:failed].any? + error!({ error: 'Some extensions failed to be granted', results: results }, 400) + end + + # Send notifications only if successful and after processing all students + if results[:successful].any? + # Use the extension comments directly from the service results (thread-safe) + successful_extensions = results[:successful].map do |result| + extension_comment = result[:extension_comment] + if extension_comment.nil? + Rails.logger.warn "No extension comment found for project #{result[:project_id]}" + nil + else + Rails.logger.debug "Using extension comment: #{extension_comment.id} for project #{result[:project_id]}" + extension_comment + end + end + + # Filter out any nil results in case a comment wasn't found + successful_extensions.compact! + Rails.logger.info "Processing #{successful_extensions.count} successful extensions for notifications" + + if successful_extensions.any? + begin + Rails.logger.info "Sending extension notifications for #{successful_extensions.count} extensions" + NotificationsMailer.extension_granted( + successful_extensions, + current_user, + params[:student_ids].count, + results[:failed], + true # is_staff_grant = true + ).deliver_now + Rails.logger.info "Extension notifications sent successfully" + rescue StandardError => e + Rails.logger.error "Failed to send extension notifications: #{e.message}" + Rails.logger.error e.backtrace.join("\n") + # Don't fail the entire request if email fails, but log the error + end + + # Create in-system notifications for successful extensions + results[:successful].each do |result| + student = User.find_by(id: result[:student_id]) + next unless student + + Notification.create!( + user_id: student.id, + message: "#{unit.code}: You were granted an extension for task '#{task_definition.name}'." + ) + end + end + end + + status 201 + present results, with: Grape::Presenters::Presenter + end + rescue ActiveRecord::RecordNotFound + error!({ error: 'Unit or task definition not found' }, 404) + rescue StandardError + error!({ error: 'An unexpected error occurred' }, 500) + end +end diff --git a/app/mailers/notifications_mailer.rb b/app/mailers/notifications_mailer.rb index 433f2e7eb..74291840d 100644 --- a/app/mailers/notifications_mailer.rb +++ b/app/mailers/notifications_mailer.rb @@ -1,7 +1,17 @@ class NotificationsMailer < ApplicationMailer + + # Load configuration values at class level + def self.doubtfire_host + Doubtfire::Application.config.institution[:host] || 'doubtfire.deakin.edu.au' + end + + def self.doubtfire_product_name + Doubtfire::Application.config.institution[:product_name] || 'Doubtfire' + end + def add_general - @doubtfire_host = Doubtfire::Application.config.institution[:host] - @doubtfire_product_name = Doubtfire::Application.config.institution[:product_name] + @doubtfire_host = self.class.doubtfire_host + @doubtfire_product_name = self.class.doubtfire_product_name @unsubscribe_url = "#{@doubtfire_host}/edit_profile" end @@ -108,6 +118,72 @@ def this_these(num) end end + # Sends a summary email to the staff member who granted the extensions + def extension_granted_summary(extensions, granted_by, total_selected, failed_extensions = []) + @granted_by = granted_by + @extensions = extensions + @total_selected = total_selected + @failed_extensions = failed_extensions + @unit = extensions.any? ? extensions.first.task.unit : nil + @is_tutor = true + + add_general + + email_with_name = %("#{@granted_by.name}" <#{@granted_by.email}>) + # Set explicit from address using product name and a default sender + from_address = %("#{self.class.doubtfire_product_name}" ) + + mail( + to: email_with_name, + from: from_address, + subject: @unit ? "#{@unit.name}: Staff Grant Extensions" : "Staff Grant Extensions", + template_name: 'extension_granted' + ) + end + + # Sends a notification to a student about their granted extension + def extension_granted_notification(extension, granted_by) + @granted_by = granted_by + @extension = extension + @task = extension.task + @student = extension.project.student + @is_tutor = false + + add_general + + email_with_name = %("#{@student.name}" <#{@student.email}>) + tutor_email = %("#{@granted_by.name}" <#{@granted_by.email}>) + + mail( + to: email_with_name, + from: tutor_email, + subject: "#{@task.unit.name}: Extension granted for #{@task.task_definition.name}", + template_name: 'extension_granted' + ) + end + + # Main method to handle extension notifications from staff + def extension_granted(extensions, granted_by, total_selected, failed_extensions = [], is_staff_grant: false) + # Only send notifications for staff-granted bulk extensions + return unless is_staff_grant && (extensions.any? || failed_extensions.any?) + + begin + # Send summary to staff member who granted the extensions + NotificationsMailer.extension_granted_summary(extensions, granted_by, total_selected, failed_extensions).deliver_now + + # Send individual notifications only to students who have enabled email notifications + extensions.each do |extension| + student = extension.project.student + if student.receive_task_notifications + NotificationsMailer.extension_granted_notification(extension, granted_by).deliver_now + end + end + rescue StandardError => e + Rails.logger.error "Failed to send extension notifications: #{e.message}" + Rails.logger.error e.backtrace.join("\n") + end + end + helper_method :top_task_desc helper_method :were_was helper_method :are_is diff --git a/app/models/notification.rb b/app/models/notification.rb new file mode 100644 index 000000000..c99183b19 --- /dev/null +++ b/app/models/notification.rb @@ -0,0 +1,3 @@ +class Notification < ApplicationRecord + belongs_to :user +end diff --git a/app/models/overseer_assessment.rb b/app/models/overseer_assessment.rb index eab52e485..82dd5cf8c 100644 --- a/app/models/overseer_assessment.rb +++ b/app/models/overseer_assessment.rb @@ -118,7 +118,7 @@ def update_assessment_comment(text) add_assessment_comment text end - def send_to_overseer() + def send_to_overseer return { error: "Your task is already queued for processing. Pleasse wait until you receive a response before queueing your task again." } if self.status == :queued # TODO: Check status and do not queue if already queued diff --git a/app/models/unit.rb b/app/models/unit.rb index 085e3968b..9106f7261 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -36,6 +36,7 @@ def self.permissions :download_jplag_report, :get_marking_sessions, :get_tutor_times, + :grant_extensions ] # What can convenors do with units? @@ -64,7 +65,8 @@ def self.permissions :get_tutor_times, :get_tutor_times_summary, :get_marking_sessions, - :upload_grades_csv + :upload_grades_csv, + :grant_extensions ] # What can admin do with units? @@ -2081,18 +2083,23 @@ def get_all_tasks_for(user, my_tutorials_only = false) group( 'sq.tutorial_id', 'sq.tutorial_stream_id', + 'tasks.id', 'task_statuses.id', 'project_id', - 'tasks.id', 'task_definition_id', 'task_definitions.start_date', - 'status_id', 'completion_date', 'times_assessed', 'submission_date', 'grade', - 'quality_pts' + 'quality_pts', + 'task_comments.id', + 'task_comments.created_at', + 'task_pins.task_id', + 'task_similarities.id', + 'task_similarities.flagged' ) + if my_tutorials_only unit_role = unit_role_for(user) unless unit_role.nil? diff --git a/app/models/user.rb b/app/models/user.rb index c360c140f..07c8b62ed 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -150,6 +150,7 @@ def token_for_text?(a_token, token_type) has_many :chip_usage, dependent: :destroy, inverse_of: :tutor, class_name: 'Feedback::ChipUsage' has_many :marking_sessions, dependent: :destroy + has_many :notifications, dependent: :destroy # Model validations/constraints validates :first_name, presence: true diff --git a/app/services/extension_service.rb b/app/services/extension_service.rb new file mode 100644 index 000000000..9dd7fdc9f --- /dev/null +++ b/app/services/extension_service.rb @@ -0,0 +1,70 @@ +class ExtensionService + def self.grant_extension(project_id, task_definition_id, user, weeks_requested, comment, is_staff_grant: false) + # Find project and task + project = Project.find(project_id) + task_definition = project.unit.task_definitions.find(task_definition_id) + task = project.task_for_task_definition(task_definition) + + # ===== Common Validation Logic (used by both endpoints) ===== + # Validate extension weeks + return { success: false, error: 'Extension weeks cannot be 0', status: 403 } if weeks_requested == 0 + + # Calculate max duration + max_duration = task.weeks_can_extend + duration = weeks_requested + duration = max_duration unless weeks_requested <= max_duration + + # Check if extension would exceed deadline + return { success: false, error: 'Extensions cannot be granted beyond task deadline', status: 403 } if duration <= 0 + + # === Flexible dates rule === + if !is_staff_grant && project.unit.allow_flexible_dates + return { + success: false, + error: 'Extensions are disabled for this unit.', + status: 403 + } + end + + # ===== Student-Initiated Extension Logic (current endpoint) ===== + unless is_staff_grant || + AuthorisationHelpers.authorise?( + user, + task, + :request_extension, + ->(role, perm_hash, other) { task.specific_permission_hash(role, perm_hash, other) } + ) + return { + success: false, + error: 'Not authorised to request an extension for this task', + status: 403 + } + end + + # ===== Staff Grant Logic (new endpoint) ===== + if is_staff_grant && + !AuthorisationHelpers.authorise?(user, project.unit, :grant_extensions) + return { + success: false, + error: 'Not authorised to grant extensions for this unit', + status: 403 + } + end + + # ===== Common Extension Logic ===== + # Apply the extension + result = task.apply_for_extension(user, comment, duration) + + # Auto-approve if it's a staff grant + if is_staff_grant + extension_comment = result.becomes(ExtensionComment) + extension_comment.assess_extension(user, true, true) + end + + { success: true, result: result, status: 201 } + rescue ActiveRecord::RecordNotFound => e + { success: false, error: 'Task or project not found', status: 404 } + rescue StandardError => e + { success: false, error: e.message, status: 500 } + end +end diff --git a/app/views/notifications_mailer/extension_granted.html.erb b/app/views/notifications_mailer/extension_granted.html.erb new file mode 100644 index 000000000..d3eb9a397 --- /dev/null +++ b/app/views/notifications_mailer/extension_granted.html.erb @@ -0,0 +1,124 @@ + + + + + + + +
+

Extension Granted

+
+ +
+ <% if @is_tutor %> +

You have granted extensions for the following students:

+ + + + + + + + + + + <% @extensions.each do |extension| %> + + + + + + <% end %> + +
StudentTaskNew Due Date
<%= extension.project.student.name %><%= extension.task.task_definition.name %><%= extension.task.due_date.strftime("%d %b %Y") %>
+ + <% if @failed_extensions.any? %> +

Failed Extensions

+ + + + + + + + + <% @failed_extensions.each do |failed| %> + + + + + <% end %> + +
Student IDError
<%= failed[:student_id] %><%= failed[:error] %>
+ <% end %> + +

Total students selected: <%= @total_selected %>

+

Successfully granted: <%= @extensions.count %>

+ <% if @failed_extensions.any? %> +

Failed: <%= @failed_extensions.count %>

+ <% end %> + <% else %> +

Dear <%= @student.name %>,

+ +

An extension has been granted for your task: <%= @task.task_definition.name %>

+ +

Details:

+ + <% end %> +
+ + + + diff --git a/app/views/notifications_mailer/extension_granted.text.erb b/app/views/notifications_mailer/extension_granted.text.erb new file mode 100644 index 000000000..f1d986465 --- /dev/null +++ b/app/views/notifications_mailer/extension_granted.text.erb @@ -0,0 +1,38 @@ +<% if @is_tutor %> +You have granted extensions for the following students: + +Extensions granted: +<% @extensions.each do |extension| %> +- <%= extension.project.student.name %>: <%= extension.task.task_definition.name %> + New due date: <%= extension.task.due_date.strftime("%B %d, %Y") %> +<% end %> + +Summary: +- Total selected for extension: <%= @total_selected %> +- Successfully granted: <%= @extensions.count %> +<% if @failed_extensions.present? %> +- Failed to grant: <%= @failed_extensions.count %> + +Failed extensions: +<% @failed_extensions.each do |failed| %> +- Student ID <%= failed[:student_id] %>: <%= failed[:error] %> +<% end %> +<% end %> +<% else %> +Dear <%= @student.name %>, + +An extension has been granted for your task: <%= @task.task_definition.name %> + +Details: +- New due date: <%= @task.due_date.strftime("%B %d, %Y") %> +- Granted by: <%= @granted_by.name %> +<% if @extension.comment.present? %> +- Comment: <%= @extension.comment %> +<% end %> +<% end %> + +Cheers, +The <%= @doubtfire_product_name %> Team + +--- +To unsubscribe from these notifications, visit: <%= @unsubscribe_url %> diff --git a/config/environments/test.rb b/config/environments/test.rb index 24fcb3d4c..b49259719 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -27,6 +27,9 @@ # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test + config.action_mailer.perform_deliveries = true + config.action_mailer.raise_delivery_errors = true + config.action_mailer.default_url_options = { host: 'test.host' } # Print deprecation notices to the stderr config.active_support.deprecation = :stderr diff --git a/db/migrate/20250518011250_create_notifications.rb b/db/migrate/20250518011250_create_notifications.rb new file mode 100644 index 000000000..0f8326726 --- /dev/null +++ b/db/migrate/20250518011250_create_notifications.rb @@ -0,0 +1,10 @@ +class CreateNotifications < ActiveRecord::Migration[7.1] + def change + create_table :notifications do |t| + t.integer :user_id + t.string :message + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 0e65657d5..91184060a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,11 +10,11 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_11_02_221253) do +ActiveRecord::Schema[8.0].define(version: 2025_12_12_010033) do create_table "activity_types", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.string "name", null: false t.string "abbreviation", null: false t.datetime "created_at", null: false + t.string "name", null: false t.datetime "updated_at", null: false t.index ["abbreviation"], name: "index_activity_types_on_abbreviation", unique: true t.index ["name"], name: "index_activity_types_on_name", unique: true @@ -22,143 +22,152 @@ create_table "auth_tokens", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.datetime "auth_token_expiry", null: false - t.bigint "user_id" t.string "authentication_token", null: false - t.integer "token_type", default: 0, null: false t.datetime "created_at", null: false + t.integer "token_type", default: 0, null: false t.datetime "updated_at", null: false + t.bigint "user_id" t.index ["token_type"], name: "index_auth_tokens_on_token_type" t.index ["user_id"], name: "index_auth_tokens_on_user_id" end create_table "breaks", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.datetime "start_date", null: false + t.datetime "created_at", null: false t.integer "number_of_weeks", null: false + t.datetime "start_date", null: false t.bigint "teaching_period_id" - t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["teaching_period_id"], name: "index_breaks_on_teaching_period_id" end create_table "campuses", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.string "name", null: false - t.integer "mode", null: false t.string "abbreviation", null: false t.boolean "active", null: false t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.integer "mode", null: false + t.string "name", null: false t.string "timezone" + t.datetime "updated_at", null: false t.index ["abbreviation"], name: "index_campuses_on_abbreviation", unique: true t.index ["active"], name: "index_campuses_on_active" t.index ["name"], name: "index_campuses_on_name", unique: true end create_table "chip_usages", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.datetime "created_at", null: false t.bigint "feedback_chip_id", null: false t.bigint "tutor_id", null: false - t.integer "usage_count", default: 0, null: false - t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.integer "usage_count", default: 0, null: false t.index ["feedback_chip_id"], name: "index_chip_usages_on_feedback_chip_id" t.index ["tutor_id"], name: "index_chip_usages_on_tutor_id" end create_table "comments_read_receipts", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "task_comment_id", null: false - t.bigint "user_id", null: false t.datetime "created_at", null: false + t.bigint "task_comment_id", null: false t.datetime "updated_at", null: false + t.bigint "user_id", null: false t.index ["task_comment_id", "user_id"], name: "index_comments_read_receipts_on_task_comment_id_and_user_id", unique: true t.index ["task_comment_id"], name: "index_comments_read_receipts_on_task_comment_id" t.index ["user_id"], name: "index_comments_read_receipts_on_user_id" end create_table "d2l_assessment_mappings", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "unit_id", null: false - t.string "org_unit_id" - t.integer "grade_object_id" t.datetime "created_at", null: false + t.integer "grade_object_id" + t.string "org_unit_id" + t.bigint "unit_id", null: false t.datetime "updated_at", null: false t.index ["unit_id"], name: "index_d2l_assessment_mappings_on_unit_id", unique: true end create_table "discussion_comments", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.datetime "time_started" - t.datetime "time_completed" + t.datetime "created_at", null: false t.integer "number_of_prompts" + t.datetime "time_completed" + t.datetime "time_started" + t.datetime "updated_at", null: false + end + + create_table "discussion_prompts", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.text "content", null: false t.datetime "created_at", null: false + t.integer "priority", default: 0 + t.bigint "task_definition_id", null: false t.datetime "updated_at", null: false + t.index ["task_definition_id"], name: "index_discussion_prompts_on_task_definition_id" end create_table "feedback_chips", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.string "type" t.text "chip_text" - t.text "description" t.text "comment_text" - t.text "summary_text" + t.datetime "created_at", null: false + t.text "description" t.bigint "learning_outcome_id", null: false t.bigint "parent_chip_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.text "summary_text" t.string "task_status" + t.string "type" + t.datetime "updated_at", null: false t.index ["learning_outcome_id"], name: "index_feedback_chips_on_learning_outcome_id" t.index ["parent_chip_id"], name: "index_feedback_chips_on_parent_chip_id" end create_table "group_memberships", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "group_id" - t.bigint "project_id" t.boolean "active", default: true t.datetime "created_at" + t.bigint "group_id" + t.bigint "project_id" t.datetime "updated_at" t.index ["group_id"], name: "index_group_memberships_on_group_id" t.index ["project_id"], name: "index_group_memberships_on_project_id" end create_table "group_sets", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "unit_id" - t.string "name" t.boolean "allow_students_to_create_groups", default: true t.boolean "allow_students_to_manage_groups", default: true - t.boolean "keep_groups_in_same_class", default: false - t.datetime "created_at" - t.datetime "updated_at" t.integer "capacity" + t.datetime "created_at" + t.boolean "keep_groups_in_same_class", default: false t.boolean "locked", default: false, null: false + t.string "name" + t.bigint "unit_id" + t.datetime "updated_at" t.index ["name", "unit_id"], name: "index_group_sets_on_name_and_unit_id", unique: true t.index ["unit_id"], name: "index_group_sets_on_unit_id" end create_table "group_submissions", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.datetime "created_at" t.bigint "group_id" t.string "notes" t.bigint "submitted_by_project_id" - t.datetime "created_at" - t.datetime "updated_at" t.bigint "task_definition_id" + t.datetime "updated_at" t.index ["group_id"], name: "index_group_submissions_on_group_id" t.index ["submitted_by_project_id"], name: "index_group_submissions_on_submitted_by_project_id" t.index ["task_definition_id"], name: "index_group_submissions_on_task_definition_id" end create_table "groups", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.integer "capacity_adjustment", default: 0, null: false + t.datetime "created_at" t.bigint "group_set_id" - t.bigint "tutorial_id" + t.boolean "locked", default: false, null: false t.string "name" - t.datetime "created_at" + t.bigint "tutorial_id" t.datetime "updated_at" - t.integer "capacity_adjustment", default: 0, null: false - t.boolean "locked", default: false, null: false t.index ["group_set_id"], name: "index_groups_on_group_set_id" t.index ["name", "group_set_id"], name: "index_groups_on_name_and_group_set_id", unique: true t.index ["tutorial_id"], name: "index_groups_on_tutorial_id" end create_table "learning_outcome_links", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.datetime "created_at", null: false + t.string "link_type" t.bigint "source_id", null: false t.bigint "target_id", null: false - t.string "link_type" - t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["source_id", "target_id"], name: "index_learning_outcome_links_on_source_id_and_target_id", unique: true t.index ["source_id"], name: "index_learning_outcome_links_on_source_id" @@ -166,85 +175,92 @@ end create_table "learning_outcomes", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.string "short_description" - t.string "full_outcome_description", limit: 4096 t.string "abbreviation" t.bigint "context_id" t.string "context_type" t.datetime "created_at", null: false + t.string "full_outcome_description", limit: 4096 + t.string "short_description" t.datetime "updated_at", null: false t.index ["abbreviation", "context_type", "context_id"], name: "index_learning_outcomes_on_abbreviation_and_context", unique: true t.index ["context_id", "context_type"], name: "index_learning_outcomes_on_context_id_and_context_type" end create_table "logins", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.datetime "timestamp" - t.bigint "user_id" t.datetime "created_at", null: false + t.datetime "timestamp" t.datetime "updated_at", null: false + t.bigint "user_id" t.index ["user_id"], name: "index_logins_on_user_id" end create_table "marking_sessions", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "user_id", null: false - t.bigint "unit_id", null: false + t.datetime "created_at", null: false + t.boolean "during_tutorial" + t.datetime "end_time" t.string "ip_address" t.datetime "start_time" - t.datetime "end_time" - t.boolean "during_tutorial" - t.datetime "created_at", null: false + t.bigint "unit_id", null: false t.datetime "updated_at", null: false + t.bigint "user_id", null: false t.index ["unit_id"], name: "index_marking_sessions_on_unit_id" t.index ["user_id", "unit_id", "ip_address", "updated_at"], name: "index_marking_sessions_on_user_unit_ip_and_time" t.index ["user_id"], name: "index_marking_sessions_on_user_id" end + create_table "notifications", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.datetime "created_at", null: false + t.string "message" + t.datetime "updated_at", null: false + t.integer "user_id" + end + create_table "overseer_assessments", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "task_id", null: false - t.string "submission_timestamp", null: false + t.datetime "created_at", null: false t.string "result_task_status" t.integer "status", default: 0, null: false - t.datetime "created_at", null: false + t.string "submission_timestamp", null: false + t.bigint "task_id", null: false t.datetime "updated_at", null: false t.index ["task_id", "submission_timestamp"], name: "index_overseer_assessments_on_task_id_and_submission_timestamp", unique: true t.index ["task_id"], name: "index_overseer_assessments_on_task_id" end create_table "overseer_images", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "last_pulled_date" t.string "name", null: false + t.integer "pulled_image_status" + t.text "pulled_image_text" t.string "tag", null: false - t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.text "pulled_image_text" - t.integer "pulled_image_status" - t.datetime "last_pulled_date" t.index ["name"], name: "index_overseer_images_on_name", unique: true t.index ["tag"], name: "index_overseer_images_on_tag", unique: true end create_table "projects", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "unit_id" - t.string "project_role" + t.bigint "assessor_id" + t.bigint "campus_id" + t.boolean "compile_portfolio", default: false t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.boolean "started" - t.string "progress" - t.string "status" - t.string "task_stats" t.boolean "enrolled", default: true - t.integer "target_grade", default: 0 - t.boolean "compile_portfolio", default: false - t.date "portfolio_production_date" - t.bigint "user_id" t.integer "grade", default: 0 t.string "grade_rationale", limit: 4096 - t.bigint "campus_id" - t.integer "submitted_grade" - t.boolean "uses_draft_learning_summary", default: false, null: false t.boolean "portfolio_auto_generated", default: false, null: false t.integer "portfolio_generation_pid" + t.date "portfolio_production_date" + t.string "progress" + t.string "project_role" t.integer "spec_con_days", default: 0, null: false - t.bigint "assessor_id" + t.boolean "started" + t.string "status" + t.integer "submitted_grade" + t.integer "target_grade", default: 0 + t.string "task_stats" + t.bigint "unit_id" + t.datetime "updated_at", null: false + t.bigint "user_id" + t.boolean "uses_draft_learning_summary", default: false, null: false t.index ["assessor_id"], name: "index_projects_on_assessor_id" t.index ["campus_id"], name: "index_projects_on_campus_id" t.index ["enrolled"], name: "index_projects_on_enrolled" @@ -254,19 +270,19 @@ end create_table "roles", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.string "name" - t.text "description" t.datetime "created_at", null: false + t.text "description" + t.string "name" t.datetime "updated_at", null: false end create_table "session_activities", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "marking_session_id", null: false t.string "action" + t.datetime "created_at", null: false + t.bigint "marking_session_id", null: false t.bigint "project_id" - t.bigint "task_id" t.bigint "task_definition_id" - t.datetime "created_at", null: false + t.bigint "task_id" t.datetime "updated_at", null: false t.index ["action", "task_id", "created_at"], name: "index_session_activities_on_action_task_created_at" t.index ["marking_session_id"], name: "index_session_activities_on_marking_session_id" @@ -276,13 +292,13 @@ end create_table "staff_notes", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.datetime "created_at", null: false t.text "note" t.bigint "project_id", null: false - t.bigint "user_id", null: false - t.bigint "staff_notes_id" t.bigint "reply_to_id" - t.datetime "created_at", null: false + t.bigint "staff_notes_id" t.datetime "updated_at", null: false + t.bigint "user_id", null: false t.index ["project_id"], name: "index_staff_notes_on_project_id" t.index ["reply_to_id"], name: "index_staff_notes_on_reply_to_id" t.index ["staff_notes_id"], name: "index_staff_notes_on_staff_notes_id" @@ -290,27 +306,27 @@ end create_table "task_comments", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "task_id", null: false - t.bigint "user_id", null: false + t.bigint "assessor_id" + t.string "attachment_extension" t.string "comment", limit: 4096 - t.datetime "created_at", null: false - t.bigint "recipient_id" + t.bigint "commentable_id" + t.string "commentable_type" t.string "content_type" - t.string "attachment_extension" - t.bigint "discussion_comment_id" - t.string "type" - t.datetime "time_discussion_started" - t.datetime "time_discussion_completed" - t.integer "number_of_prompts" + t.datetime "created_at", null: false t.datetime "date_extension_assessed" + t.bigint "discussion_comment_id" t.boolean "extension_granted" - t.bigint "assessor_id" - t.bigint "task_status_id" - t.integer "extension_weeks" t.string "extension_response" + t.integer "extension_weeks" + t.integer "number_of_prompts" + t.bigint "recipient_id" t.bigint "reply_to_id" - t.bigint "commentable_id" - t.string "commentable_type" + t.bigint "task_id", null: false + t.bigint "task_status_id" + t.datetime "time_discussion_completed" + t.datetime "time_discussion_started" + t.string "type" + t.bigint "user_id", null: false t.index ["assessor_id"], name: "index_task_comments_on_assessor_id" t.index ["commentable_type", "commentable_id"], name: "index_task_comments_on_commentable_type_and_commentable_id" t.index ["discussion_comment_id"], name: "index_task_comments_on_discussion_comment_id" @@ -322,38 +338,38 @@ end create_table "task_definitions", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "unit_id" - t.string "name" - t.string "description", limit: 4096 - t.decimal "weighting", precision: 10 - t.datetime "target_date", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false t.string "abbreviation" - t.string "upload_requirements", limit: 4096 - t.integer "target_grade", default: 0 - t.boolean "restrict_status_updates", default: false - t.string "plagiarism_report_url" - t.boolean "plagiarism_updated", default: false - t.integer "plagiarism_warn_pct", default: 50 - t.bigint "group_set_id" + t.boolean "assess_in_portfolio_only", default: false, null: false + t.boolean "assessment_enabled", default: false + t.datetime "created_at", null: false + t.string "description", limit: 4096 t.datetime "due_date" - t.datetime "start_date", null: false + t.bigint "group_set_id" t.boolean "is_graded", default: false + t.boolean "lock_assessments_to_tutorial_stream", default: false, null: false t.integer "max_quality_pts", default: 0 - t.bigint "tutorial_stream_id" - t.boolean "assessment_enabled", default: false + t.string "name" t.bigint "overseer_image_id" - t.string "tii_group_id" - t.string "similarity_language" - t.boolean "scorm_enabled", default: false + t.string "plagiarism_report_url" + t.boolean "plagiarism_updated", default: false + t.integer "plagiarism_warn_pct", default: 50 + t.boolean "restrict_status_updates", default: false t.boolean "scorm_allow_review", default: false + t.integer "scorm_attempt_limit", default: 0 t.boolean "scorm_bypass_test", default: false + t.boolean "scorm_enabled", default: false t.boolean "scorm_time_delay_enabled", default: false - t.integer "scorm_attempt_limit", default: 0 - t.boolean "assess_in_portfolio_only", default: false, null: false + t.string "similarity_language" + t.datetime "start_date", null: false + t.datetime "target_date", null: false + t.integer "target_grade", default: 0 + t.string "tii_group_id" + t.bigint "tutorial_stream_id" + t.bigint "unit_id" + t.datetime "updated_at", null: false + t.string "upload_requirements", limit: 4096 t.boolean "use_resources_for_jplag_base_code", default: false, null: false - t.boolean "lock_assessments_to_tutorial_stream", default: false, null: false + t.decimal "weighting", precision: 10 t.index ["abbreviation", "unit_id"], name: "index_task_definitions_on_abbreviation_and_unit_id", unique: true t.index ["group_set_id"], name: "index_task_definitions_on_group_set_id" t.index ["name", "unit_id"], name: "index_task_definitions_on_name_and_unit_id", unique: true @@ -363,29 +379,29 @@ end create_table "task_engagements", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.datetime "engagement_time" + t.datetime "created_at", null: false t.string "engagement" + t.datetime "engagement_time" t.bigint "task_id" - t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["task_id"], name: "index_task_engagements_on_task_id" end create_table "task_pins", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "task_id", null: false - t.bigint "user_id", null: false t.datetime "created_at", null: false + t.bigint "task_id", null: false t.datetime "updated_at", null: false + t.bigint "user_id", null: false t.index ["task_id", "user_id"], name: "index_task_pins_on_task_id_and_user_id", unique: true t.index ["task_id"], name: "index_task_pins_on_task_id" t.index ["user_id"], name: "fk_rails_915df186ed" end create_table "task_prerequisites", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "task_definition_id", null: false + t.datetime "created_at", null: false t.bigint "prerequisite_id", null: false + t.bigint "task_definition_id", null: false t.bigint "task_status_id", null: false - t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["prerequisite_id"], name: "index_task_prerequisites_on_prerequisite_id" t.index ["task_definition_id", "prerequisite_id"], name: "idx_on_task_definition_id_prerequisite_id_90b47ca126", unique: true @@ -394,59 +410,59 @@ end create_table "task_similarities", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "task_id" + t.datetime "created_at" + t.boolean "flagged", default: false t.bigint "other_task_id" t.integer "pct" - t.datetime "created_at" - t.datetime "updated_at" t.string "plagiarism_report_url" - t.boolean "flagged", default: false - t.string "type" + t.bigint "task_id" t.bigint "tii_submission_id" + t.string "type" + t.datetime "updated_at" t.index ["other_task_id"], name: "index_task_similarities_on_other_task_id" t.index ["task_id"], name: "index_task_similarities_on_task_id" t.index ["tii_submission_id"], name: "index_task_similarities_on_tii_submission_id" end create_table "task_statuses", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.string "name" - t.string "description" t.datetime "created_at", null: false + t.string "description" + t.string "name" t.datetime "updated_at", null: false end create_table "task_submissions", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.datetime "submission_time" t.datetime "assessment_time" + t.bigint "assessor_id" + t.datetime "created_at", null: false t.string "outcome" + t.datetime "submission_time" t.bigint "task_id" - t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.bigint "assessor_id" t.index ["assessor_id"], name: "index_task_submissions_on_assessor_id" t.index ["task_id"], name: "index_task_submissions_on_task_id" end create_table "tasks", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "task_definition_id" - t.bigint "project_id" - t.bigint "task_status_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "assessment_date" t.date "completion_date" - t.string "portfolio_evidence" - t.boolean "include_in_portfolio", default: true - t.datetime "file_uploaded_at" - t.bigint "group_submission_id" t.integer "contribution_pct", default: 100 - t.integer "times_assessed", default: 0 - t.datetime "submission_date" - t.datetime "assessment_date" - t.integer "grade" t.integer "contribution_pts", default: 3 - t.integer "quality_pts", default: -1 + t.datetime "created_at", null: false t.integer "extensions", default: 0, null: false + t.datetime "file_uploaded_at" + t.integer "grade" + t.bigint "group_submission_id" + t.boolean "include_in_portfolio", default: true + t.string "portfolio_evidence" + t.bigint "project_id" + t.integer "quality_pts", default: -1 t.integer "scorm_extensions", default: 0, null: false + t.datetime "submission_date" + t.bigint "task_definition_id" + t.bigint "task_status_id" + t.integer "times_assessed", default: 0 + t.datetime "updated_at", null: false t.index ["group_submission_id"], name: "index_tasks_on_group_submission_id" t.index ["project_id", "task_definition_id"], name: "tasks_uniq_proj_task_def", unique: true t.index ["project_id"], name: "index_tasks_on_project_id" @@ -455,43 +471,43 @@ end create_table "teaching_periods", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.string "period", null: false - t.datetime "start_date", null: false - t.datetime "end_date", null: false - t.integer "year", null: false t.datetime "active_until", null: false t.datetime "created_at", null: false + t.datetime "end_date", null: false + t.string "period", null: false + t.datetime "start_date", null: false t.datetime "updated_at", null: false + t.integer "year", null: false t.index ["period", "year"], name: "index_teaching_periods_on_period_and_year", unique: true end create_table "test_attempts", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "task_id" t.datetime "attempted_time", null: false - t.boolean "terminated", default: false - t.boolean "completion_status", default: false - t.boolean "success_status", default: false - t.float "score_scaled", default: 0.0 t.text "cmi_datamodel" + t.boolean "completion_status", default: false t.datetime "created_at", null: false + t.float "score_scaled", default: 0.0 + t.boolean "success_status", default: false + t.bigint "task_id" + t.boolean "terminated", default: false t.datetime "updated_at", null: false t.index ["task_id"], name: "index_test_attempts_on_task_id" end create_table "tii_actions", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.string "entity_type" - t.bigint "entity_id" - t.string "type" t.boolean "complete", default: false, null: false - t.integer "retries", default: 0, null: false - t.datetime "last_run" t.datetime "complete_at" - t.boolean "retry", default: true, null: false - t.integer "error_code" + t.datetime "created_at", null: false t.text "custom_error_message" + t.bigint "entity_id" + t.string "entity_type" + t.integer "error_code" + t.datetime "last_run" t.text "log" t.string "params", limit: 1024, default: "{}" - t.datetime "created_at", null: false + t.integer "retries", default: 0, null: false + t.boolean "retry", default: true, null: false + t.string "type" t.datetime "updated_at", null: false t.index ["complete"], name: "index_tii_actions_on_complete" t.index ["entity_type", "entity_id"], name: "index_tii_actions_on_entity" @@ -499,29 +515,29 @@ end create_table "tii_group_attachments", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "task_definition_id", null: false + t.datetime "created_at", null: false + t.string "file_sha1_digest" t.string "filename", null: false t.string "group_attachment_id" - t.string "file_sha1_digest" t.integer "status", default: 0, null: false - t.datetime "created_at", null: false + t.bigint "task_definition_id", null: false t.datetime "updated_at", null: false t.index ["task_definition_id"], name: "index_tii_group_attachments_on_task_definition_id" end create_table "tii_submissions", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "task_id", null: false - t.bigint "tii_task_similarity_id" - t.bigint "submitted_by_user_id", null: false + t.datetime "created_at", null: false t.string "filename", null: false t.integer "idx", null: false - t.string "submission_id" + t.integer "overall_match_percentage" t.string "similarity_pdf_id" - t.datetime "submitted_at" t.datetime "similarity_request_at" t.integer "status", default: 0, null: false - t.integer "overall_match_percentage" - t.datetime "created_at", null: false + t.string "submission_id" + t.datetime "submitted_at" + t.bigint "submitted_by_user_id", null: false + t.bigint "task_id", null: false + t.bigint "tii_task_similarity_id" t.datetime "updated_at", null: false t.index ["submitted_by_user_id"], name: "index_tii_submissions_on_submitted_by_user_id" t.index ["task_id"], name: "index_tii_submissions_on_task_id" @@ -530,21 +546,21 @@ create_table "tutorial_enrolments", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.datetime "created_at", null: false - t.datetime "updated_at", null: false t.bigint "project_id", null: false t.bigint "tutorial_id", null: false + t.datetime "updated_at", null: false t.index ["project_id"], name: "index_tutorial_enrolments_on_project_id" t.index ["tutorial_id", "project_id"], name: "index_tutorial_enrolments_on_tutorial_id_and_project_id", unique: true t.index ["tutorial_id"], name: "index_tutorial_enrolments_on_tutorial_id" end create_table "tutorial_streams", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.string "name", null: false t.string "abbreviation", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false t.bigint "activity_type_id", null: false + t.datetime "created_at", null: false + t.string "name", null: false t.bigint "unit_id", null: false + t.datetime "updated_at", null: false t.index ["abbreviation", "unit_id"], name: "index_tutorial_streams_on_abbreviation_and_unit_id", unique: true t.index ["abbreviation"], name: "index_tutorial_streams_on_abbreviation" t.index ["activity_type_id"], name: "fk_rails_14ef80da76" @@ -553,18 +569,18 @@ end create_table "tutorials", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "unit_id" - t.string "meeting_day" - t.string "meeting_time" - t.string "meeting_location" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "code" - t.bigint "unit_role_id" t.string "abbreviation" - t.integer "capacity", default: -1 t.bigint "campus_id" + t.integer "capacity", default: -1 + t.string "code" + t.datetime "created_at", null: false + t.string "meeting_day" + t.string "meeting_location" + t.string "meeting_time" t.bigint "tutorial_stream_id" + t.bigint "unit_id" + t.bigint "unit_role_id" + t.datetime "updated_at", null: false t.index ["abbreviation", "unit_id"], name: "index_tutorials_on_abbreviation_and_unit_id", unique: true t.index ["campus_id"], name: "index_tutorials_on_campus_id" t.index ["tutorial_stream_id"], name: "index_tutorials_on_tutorial_stream_id" @@ -573,13 +589,13 @@ end create_table "unit_roles", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "user_id" - t.bigint "tutorial_id" t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.boolean "observer_only", default: false t.bigint "role_id" + t.bigint "tutorial_id" t.bigint "unit_id" - t.boolean "observer_only", default: false + t.datetime "updated_at", null: false + t.bigint "user_id" t.index ["role_id"], name: "index_unit_roles_on_role_id" t.index ["tutorial_id"], name: "index_unit_roles_on_tutorial_id" t.index ["unit_id"], name: "index_unit_roles_on_unit_id" @@ -587,33 +603,33 @@ end create_table "units", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.string "name" - t.string "description", limit: 4096 - t.datetime "start_date" - t.datetime "end_date" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "code" t.boolean "active", default: true - t.datetime "last_plagarism_scan" - t.bigint "teaching_period_id" - t.bigint "main_convenor_id" + t.boolean "allow_flexible_dates", default: false, null: false + t.boolean "allow_student_change_tutorial", default: true, null: false + t.boolean "allow_student_extension_requests", default: true, null: false + t.boolean "archived", default: false + t.boolean "assessment_enabled", default: true t.boolean "auto_apply_extension_before_deadline", default: true, null: false - t.boolean "send_notifications", default: true, null: false - t.boolean "enable_sync_timetable", default: true, null: false - t.boolean "enable_sync_enrolments", default: true, null: false + t.string "code" + t.datetime "created_at", null: false + t.string "description", limit: 4096 t.bigint "draft_task_definition_id" - t.boolean "allow_student_extension_requests", default: true, null: false + t.boolean "enable_sync_enrolments", default: true, null: false + t.boolean "enable_sync_timetable", default: true, null: false + t.datetime "end_date" t.integer "extension_weeks_on_resubmit_request", default: 1, null: false - t.boolean "allow_student_change_tutorial", default: true, null: false - t.boolean "assessment_enabled", default: true + t.datetime "last_plagarism_scan" + t.bigint "main_convenor_id" + t.boolean "mark_late_submissions_as_assess_in_portfolio", default: false, null: false + t.string "name" t.bigint "overseer_image_id" t.datetime "portfolio_auto_generation_date" - t.string "tii_group_context_id" - t.boolean "archived", default: false - t.boolean "allow_flexible_dates", default: false, null: false t.datetime "portfolio_due_date" - t.boolean "mark_late_submissions_as_assess_in_portfolio", default: false, null: false + t.boolean "send_notifications", default: true, null: false + t.datetime "start_date" + t.bigint "teaching_period_id" + t.string "tii_group_context_id" + t.datetime "updated_at", null: false t.index ["draft_task_definition_id"], name: "index_units_on_draft_task_definition_id" t.index ["main_convenor_id"], name: "index_units_on_main_convenor_id" t.index ["overseer_image_id"], name: "index_units_on_overseer_image_id" @@ -621,53 +637,53 @@ end create_table "user_oauth_states", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "user_id", null: false - t.string "state" t.datetime "created_at", null: false + t.string "state" t.datetime "updated_at", null: false + t.bigint "user_id", null: false t.index ["state"], name: "index_user_oauth_states_on_state", unique: true t.index ["user_id"], name: "index_user_oauth_states_on_user_id" end create_table "user_oauth_tokens", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "user_id", null: false + t.datetime "created_at", null: false + t.datetime "expires_at" t.integer "provider", default: 0, null: false t.text "token" - t.datetime "expires_at" - t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.bigint "user_id", null: false t.index ["user_id"], name: "index_user_oauth_tokens_on_user_id" end create_table "users", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.string "email", default: "", null: false - t.string "encrypted_password", default: "", null: false - t.string "reset_password_token" - t.datetime "reset_password_sent_at" - t.datetime "remember_created_at" - t.integer "sign_in_count", default: 0 + t.datetime "created_at", null: false t.datetime "current_sign_in_at" - t.datetime "last_sign_in_at" t.string "current_sign_in_ip" - t.string "last_sign_in_ip" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false t.string "first_name" + t.boolean "has_run_first_time_setup", default: false t.string "last_name" - t.string "username" + t.datetime "last_sign_in_at" + t.string "last_sign_in_ip" + t.string "login_id" t.string "nickname" - t.string "unlock_token" - t.bigint "role_id", default: 0 - t.boolean "receive_task_notifications", default: true + t.boolean "opt_in_to_research" t.boolean "receive_feedback_notifications", default: true t.boolean "receive_portfolio_notifications", default: true - t.boolean "opt_in_to_research" - t.boolean "has_run_first_time_setup", default: false - t.string "login_id" + t.boolean "receive_task_notifications", default: true + t.datetime "remember_created_at" + t.datetime "reset_password_sent_at" + t.string "reset_password_token" + t.bigint "role_id", default: 0 + t.integer "sign_in_count", default: 0 t.string "student_id" - t.string "tii_eula_version" t.datetime "tii_eula_date" + t.string "tii_eula_version" t.boolean "tii_eula_version_confirmed", default: false, null: false + t.string "unlock_token" + t.datetime "updated_at", null: false + t.string "username" t.index ["email"], name: "index_users_on_email", unique: true t.index ["login_id"], name: "index_users_on_login_id", unique: true t.index ["role_id"], name: "index_users_on_role_id" @@ -676,23 +692,23 @@ end create_table "webcal_unit_exclusions", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| - t.bigint "webcal_id", null: false - t.bigint "unit_id", null: false t.datetime "created_at", null: false + t.bigint "unit_id", null: false t.datetime "updated_at", null: false + t.bigint "webcal_id", null: false t.index ["unit_id", "webcal_id"], name: "index_webcal_unit_exclusions_on_unit_id_and_webcal_id", unique: true t.index ["unit_id"], name: "index_webcal_unit_exclusions_on_unit_id" t.index ["webcal_id"], name: "fk_rails_d5fab02cb7" end create_table "webcals", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.datetime "created_at", null: false t.string "guid", limit: 36, null: false t.boolean "include_start_dates", default: false, null: false - t.bigint "user_id" t.integer "reminder_time" t.string "reminder_unit" - t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.bigint "user_id" t.index ["guid"], name: "index_webcals_on_guid", unique: true t.index ["user_id"], name: "index_webcals_on_user_id", unique: true end diff --git a/test/api/d2l_test.rb b/test/api/d2l_test.rb index 37eadc7ab..6c66e15cb 100644 --- a/test/api/d2l_test.rb +++ b/test/api/d2l_test.rb @@ -396,7 +396,10 @@ def test_post_grades assert_includes result[1], "Success,#{p1.student.student_id},#{p1.grade},Posted grade for #{p1.student.username}" assert_includes result[2], "Success,#{p2.student.student_id} - somehow mismatch,#{p2.grade},Posted grade for #{p2.student.username}" assert_includes result[3], "Skipped,#{p3.student.student_id} - somehow mismatch,\"\",No grade for #{p3.student.username}" - assert_includes result[4], "Error,#{s1.student_id},\"\",No OnTrack result for" + assert_match( + /Error,#{s1.student_id},"",No (OnTrack|Doubtfire) result for/, + result[4] + ) assert_includes result[5], "Error,#{p4.student.username},#{p4.grade},Not found in D2L" add_auth_header_for(user: unit.main_convenor_user) diff --git a/test/api/staff_grant_extension_test.rb b/test/api/staff_grant_extension_test.rb new file mode 100644 index 000000000..9c7c8af7c --- /dev/null +++ b/test/api/staff_grant_extension_test.rb @@ -0,0 +1,255 @@ +require 'test_helper' + +class StaffGrantExtensionTest < ActiveSupport::TestCase + include Rack::Test::Methods + include TestHelpers::AuthHelper + include TestHelpers::JsonHelper + + def app + Rails.application + end + + def test_staff_grant_extension_success + unit = FactoryBot.create(:unit) + project = unit.projects.first + staff = FactoryBot.create(:user, role: Role.tutor) + unit.employ_staff(staff, Role.tutor) + + td = TaskDefinition.new({ + unit_id: unit.id, + tutorial_stream: unit.tutorial_streams.first, + name: 'Staff Grant Extension Test', + description: 'Test task for staff grant extension', + weighting: 4, + target_grade: 0, + start_date: Time.zone.now - 1.week, + target_date: Time.zone.now + 1.week, + due_date: Time.zone.now + 2.weeks, + abbreviation: 'STAFFGRANTTEST', + restrict_status_updates: false, + upload_requirements: [], + plagiarism_warn_pct: 0.8, + is_graded: false, + max_quality_pts: 0 + }) + td.save! + data_to_post = { + student_ids: [project.student.id], + task_definition_id: td.id, + weeks_requested: 1, + comment: 'Staff granted extension' + } + + add_auth_header_for user: staff + post_json "/api/units/#{unit.id}/staff-grant-extension", data_to_post + assert_equal 201, last_response.status + + response = last_response_body + assert response["successful"].length == 1, 'Should have one successful extension' + assert response["failed"].empty?, 'Should have no failed extensions' + assert response["successful"][0]["student_id"] == project.student.id, 'Should match the student ID' + assert response["successful"][0]["weeks_requested"] == 1, 'Should have requested 1 week' + assert response["successful"][0]["extension_response"].present?, 'Should have extension response' + assert response["successful"][0]["task_status"].present?, 'Should have task status' + + notifications = Notification.where(user_id: project.student.id) + assert_equal 1, notifications.count, 'Should create one notification for the student' + notification = notifications.first + assert_match /You were granted an extension for task/, notification.message + assert_match /#{td.name}/, notification.message + assert_match /#{unit.code}/, notification.message + + td.destroy! + unit.destroy! + end + + def test_staff_grant_extension_unauthorized + unit = FactoryBot.create(:unit) + project = unit.projects.first + student = project.student # Using student instead of staff + td = unit.task_definitions.first + + data_to_post = { + student_ids: [project.student.id], + task_definition_id: td.id, + weeks_requested: 1, + comment: 'Unauthorized attempt' + } + + add_auth_header_for user: student + post_json "/api/units/#{unit.id}/staff-grant-extension", data_to_post + assert_equal 403, last_response.status, 'Should not allow non-staff to grant extensions' + end + + def test_staff_grant_extension_invalid_weeks + unit = FactoryBot.create(:unit) + project = unit.projects.first + staff = FactoryBot.create(:user, role: Role.tutor) + unit.employ_staff(staff, Role.tutor) + td = unit.task_definitions.first + + data_to_post = { + student_ids: [project.student.id], + task_definition_id: td.id, + weeks_requested: 0, + comment: 'Invalid weeks' + } + + add_auth_header_for user: staff + post_json "/api/units/#{unit.id}/staff-grant-extension", data_to_post + assert_equal 403, last_response.status, 'Should not allow 0 weeks extension' + end + + def test_staff_grant_extension_negative_weeks + unit = FactoryBot.create(:unit) + project = unit.projects.first + staff = FactoryBot.create(:user, role: Role.tutor) + unit.employ_staff(staff, Role.tutor) + td = unit.task_definitions.first + + data_to_post = { + student_ids: [project.student.id], + task_definition_id: td.id, + weeks_requested: -1, + comment: 'Negative weeks' + } + + add_auth_header_for user: staff + post_json "/api/units/#{unit.id}/staff-grant-extension", data_to_post + assert_equal 403, last_response.status, 'Should not allow negative weeks extension' + end + + def test_staff_grant_extension_missing_params + unit = FactoryBot.create(:unit) + staff = FactoryBot.create(:user, role: Role.tutor) + unit.employ_staff(staff, Role.tutor) + + data_to_post = { + student_ids: [1], + # Missing task_definition_id and weeks_requested + comment: 'Missing params' + } + + add_auth_header_for user: staff + post_json "/api/units/#{unit.id}/staff-grant-extension", data_to_post + assert_equal 400, last_response.status, 'Should require all parameters' + end + + def test_staff_grant_extension_transaction_rollback + unit = FactoryBot.create(:unit) + project = unit.projects.first + staff = FactoryBot.create(:user, role: Role.tutor) + unit.employ_staff(staff, Role.tutor) + td = unit.task_definitions.first + + # Test case 1: One valid student, one skipped student + data_to_post = { + student_ids: [project.student.id, 999999], # One valid, one invalid + task_definition_id: td.id, + weeks_requested: 1, + comment: 'Transaction test with skipped student' + } + + add_auth_header_for user: staff + post_json "/api/units/#{unit.id}/staff-grant-extension", data_to_post + assert_equal 201, last_response.status, 'Should succeed for valid student' + + response = last_response_body + assert response["successful"].length == 1, 'Should have one successful extension' + assert response["skipped"].length == 1, 'Should have one skipped student' + assert response["failed"].empty?, 'Should have no failed extensions' + assert response["skipped"][0]["student_id"] == 999999, 'Should have skipped the invalid student ID' + assert response["skipped"][0]["reason"] == 'Student not found in unit', 'Should have correct skip reason' + + # Verify only the valid student got an extension + task = project.task_for_task_definition(td) + assert task.extensions == 1, 'Should have one extension for the valid student' + + # Test case 2: Test actual transaction rollback + # Create a second project to test with + project2 = unit.projects.create!( + user: FactoryBot.create(:user, role: Role.student), + enrolled: true + ) + + # First, grant extensions to both students + data_to_post = { + student_ids: [project.student.id, project2.student.id], + task_definition_id: td.id, + weeks_requested: 1, + comment: 'Initial extensions' + } + + add_auth_header_for user: staff + post_json "/api/units/#{unit.id}/staff-grant-extension", data_to_post + assert_equal 201, last_response.status, 'Should succeed for both students' + + # Verify both students got extensions + task1 = project.task_for_task_definition(td) + task2 = project2.task_for_task_definition(td) + assert task1.extensions == 2, 'First student should have two extensions' + assert task2.extensions == 1, 'Second student should have one extension' + + # Now try to grant extensions with a task that would cause a failure + # Use a task that's past its deadline to force a failure + td.due_date = Time.zone.now - 1.day + td.save! + + data_to_post = { + student_ids: [project.student.id, project2.student.id], + task_definition_id: td.id, + weeks_requested: 1, + comment: 'Transaction rollback test' + } + + add_auth_header_for user: staff + post_json "/api/units/#{unit.id}/staff-grant-extension", data_to_post + assert_equal 403, last_response.status, 'Should fail with 403 when task is past deadline' + + # Verify neither student got a new extension (transaction rolled back) + task1.reload + task2.reload + assert task1.extensions == 2, 'First student should still have two extensions' + assert task2.extensions == 1, 'Second student should still have one extension' + + td.destroy! + unit.destroy! + end + + def test_staff_grant_extension_invalid_unit + unit = FactoryBot.create(:unit) + project = unit.projects.first + staff = FactoryBot.create(:user, role: Role.tutor) + unit.employ_staff(staff, Role.tutor) + td = unit.task_definitions.first + + data_to_post = { + student_ids: [project.student.id], + task_definition_id: td.id, + weeks_requested: 1, + comment: 'Invalid unit' + } + + add_auth_header_for user: staff + post_json "/api/units/999999/staff-grant-extension", data_to_post + assert_equal 404, last_response.status, 'Should return 404 for invalid unit' + end + + def test_staff_grant_extension_invalid_task + unit = FactoryBot.create(:unit) + project = unit.projects.first + staff = FactoryBot.create(:user, role: Role.tutor) + unit.employ_staff(staff, Role.tutor) + + data_to_post = { + student_ids: [project.student.id], + task_definition_id: 999999, + weeks_requested: 1, + comment: 'Invalid task' + } + + add_auth_header_for user: staff + post_json "/api/units/#{unit.id}/staff-grant-extension", data_to_post + assert_equal 404, last_response.status, 'Should return 404 for invalid task definition' + end +end diff --git a/test/api/tasks_api_test.rb b/test/api/tasks_api_test.rb index 3ab4e7851..e97bc3bc4 100644 --- a/test/api/tasks_api_test.rb +++ b/test/api/tasks_api_test.rb @@ -979,7 +979,6 @@ def test_resubmission_doesnt_change_submission_date assert_equal 201, last_response.status, last_response_body tasks = unit.tasks_for_task_inbox(tutor, false) - assert_equal project1.id, tasks.first.project.id, "First task in inbox should be project1's task" assert_equal project2.id, tasks.second.project.id, "Second task in inbox should be project2's task" diff --git a/test/mailers/notifications_mailer_test.rb b/test/mailers/notifications_mailer_test.rb new file mode 100644 index 000000000..a8a92358e --- /dev/null +++ b/test/mailers/notifications_mailer_test.rb @@ -0,0 +1,232 @@ +require 'test_helper' + +class NotificationsMailerTest < ActionMailer::TestCase + include TestHelpers::AuthHelper + + def setup + # Mock Doubtfire configuration + Doubtfire::Application.config.institution = { + host: 'doubtfire.deakin.edu.au', + product_name: 'Doubtfire' + } + + # Create unit and staff + @unit = FactoryBot.create(:unit) + @staff = FactoryBot.create(:user, role: Role.tutor) + @unit.employ_staff(@staff, Role.tutor) + + # Create a task definition + @task_definition = @unit.task_definitions.create!({ + tutorial_stream: @unit.tutorial_streams.first, + name: 'Test Task', + description: 'Test task for notifications', + weighting: 4, + target_grade: 0, + start_date: Time.zone.now - 1.week, + target_date: Time.zone.now + 1.week, + due_date: Time.zone.now + 2.weeks, + abbreviation: 'TESTTASK', + restrict_status_updates: false, + upload_requirements: [], + plagiarism_warn_pct: 0.8, + is_graded: false, + max_quality_pts: 0 + }) + + # Create students and projects with notification preferences + @students = [] + @projects = [] + + # Create one student with notifications enabled + student_with_notifications = FactoryBot.create(:user, role: Role.student) + student_with_notifications.update(receive_task_notifications: true) + project = @unit.projects.create!(user: student_with_notifications, enrolled: true) + @students << student_with_notifications + @projects << project + + # Create two students without notifications + 2.times do + student = FactoryBot.create(:user, role: Role.student) + student.update(receive_task_notifications: false) + project = @unit.projects.create!(user: student, enrolled: true) + @students << student + @projects << project + end + + # Clear any existing emails before each test + ActionMailer::Base.deliveries.clear + end + + def teardown + # @task_definition.destroy! + # @unit.destroy! + ActionMailer::Base.deliveries.clear + end + + test 'creates correct extension summary email' do + # Create extensions + extensions = [] + @projects.each do |project| + task = project.task_for_task_definition(@task_definition) + extension = task.apply_for_extension(@staff, 'Test comment', 1) + extension.assess_extension(@staff, true, true) + extensions << extension + end + + # Get the mail object + mail = NotificationsMailer.extension_granted_summary(extensions, @staff, extensions.count) + + # Verify email properties + assert_equal [@staff.email], mail.to + assert_equal "#{@unit.name}: Staff Grant Extensions", mail.subject + assert_match /You have granted extensions for the following students/, mail.html_part.body.to_s + + # Verify from address contains no-reply + assert_includes mail.from.first, "no-reply@" + assert_includes mail.from.first, NotificationsMailer.doubtfire_host + end + + test 'creates correct extension notification email' do + # Create extension + project = @projects.first + task = project.task_for_task_definition(@task_definition) + extension = task.apply_for_extension(@staff, 'Test comment', 1) + extension.assess_extension(@staff, true, true) + + # Get the mail object + mail = NotificationsMailer.extension_granted_notification(extension, @staff) + + # Verify email properties + assert_equal [@students.first.email], mail.to + assert_equal "#{@unit.name}: Extension granted for #{@task_definition.name}", mail.subject + assert_match /Dear #{@students.first.first_name}/, mail.html_part.body.to_s + + # Verify from address contains staff email + assert_includes mail.from.first, @staff.email + end + + test 'creates correct extension summary with failed extensions' do + # Create successful extensions + successful_extensions = [] + @projects.each do |project| + task = project.task_for_task_definition(@task_definition) + extension = task.apply_for_extension(@staff, 'Test comment', 1) + extension.assess_extension(@staff, true, true) + successful_extensions << extension + end + + # Create failed extensions + failed_extensions = [ + { student_id: 999, error: 'Student not found in unit' }, + { student_id: 1000, error: 'Extension cannot be granted beyond task deadline' } + ] + + # Get the mail object + mail = NotificationsMailer.extension_granted_summary( + successful_extensions, + @staff, + successful_extensions.count, + failed_extensions + ) + + # Verify email includes failed extensions + assert_equal [@staff.email], mail.to + assert_match /Failed Extensions/, mail.html_part.body.to_s + assert_match /999/, mail.html_part.body.to_s + assert_match /1000/, mail.html_part.body.to_s + + # Verify from address contains no-reply + assert_includes mail.from.first, "no-reply@" + assert_includes mail.from.first, NotificationsMailer.doubtfire_host + end + + test 'creates correct extension notification with special characters' do + # Create task with special characters + special_task = @unit.task_definitions.create!({ + tutorial_stream: @unit.tutorial_streams.first, + name: 'Test Task with !@#$%^&*()', + description: 'Test task with special characters', + weighting: 4, + target_grade: 0, + start_date: Time.zone.now - 1.week, + target_date: Time.zone.now + 1.week, + due_date: Time.zone.now + 2.weeks, + abbreviation: 'SPECIAL', + restrict_status_updates: false, + upload_requirements: [], + plagiarism_warn_pct: 0.8, + is_graded: false, + max_quality_pts: 0 + }) + + # Create extension + project = @projects.first + task = project.task_for_task_definition(special_task) + extension = task.apply_for_extension(@staff, 'Special characters test', 1) + extension.assess_extension(@staff, true, true) + + # Get the mail object + mail = NotificationsMailer.extension_granted_notification(extension, @staff) + + # Verify email handles special characters + assert_equal [@students.first.email], mail.to + assert_equal "#{@unit.name}: Extension granted for #{special_task.name}", mail.subject + assert_match /Dear #{@students.first.name}/, mail.html_part.body.to_s + + # Verify from address contains staff email + assert_includes mail.from.first, @staff.email + + # Clean up + special_task.destroy! + end + + test 'creates correct weekly staff summary email' do + # Create data for summary stats + summary_stats = { + unit: @unit, + week_start: Time.zone.now - 1.week, + week_end: Time.zone.now, + staff: {} + } + + unit_role = @unit.unit_roles.find_by(user: @staff) + summary_stats[:staff][unit_role.user] = { + tasks_awaiting_feedback_count: 1, + weekly_engagements_count: 2, + staff_engagements: 3, + oldest_task_days: 4, + weekly_total_tasks_discussed: 5 + } + + # Get the mail object + mail = NotificationsMailer.weekly_staff_summary(unit_role, summary_stats) + + # Verify email properties + assert_equal [@staff.email], mail.to + assert_equal "#{@unit.name}: Weekly Summary", mail.subject + + # Verify from address contains convenor email + assert_includes mail.from.first, @unit.main_convenor_user.email + end + + test 'creates correct weekly student summary email' do + # Create data for summary stats + summary_stats = { + unit: @unit, + week_start: Time.zone.now - 1.week, + week_end: Time.zone.now + } + + project = @projects.first + + # Get the mail object + mail = NotificationsMailer.weekly_student_summary(project, summary_stats, false) + + # Verify email properties + assert_equal [@students.first.email], mail.to + assert_equal "#{@unit.name}: Weekly Summary", mail.subject + + # Verify from address contains tutor email + assert_includes mail.from.first, project.main_convenor_user.email + end +end