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/CHANGELOG.md b/CHANGELOG.md index e2be50929..cedbfbd4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,61 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [10.0.0-65](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-64...v10.0.0-65) (2026-01-05) + + +### Features + +* attention required task status ([#557](https://github.com/b0ink/doubtfire-deploy/issues/557)) ([88e0c7a](https://github.com/b0ink/doubtfire-deploy/commit/88e0c7a9e65a71aed0b9a8453d75af0aa52a09f4)) +* custom project task deadlines ([#550](https://github.com/b0ink/doubtfire-deploy/issues/550)) ([2a50638](https://github.com/b0ink/doubtfire-deploy/commit/2a5063848d9cde0b0277f6e2463613945d720b99)) +* overseer pipeline ([#559](https://github.com/b0ink/doubtfire-deploy/issues/559)) ([d2e49bf](https://github.com/b0ink/doubtfire-deploy/commit/d2e49bfd57e69281923458b8853226a85c9da390)) + +## [10.0.0-64](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-63...v10.0.0-64) (2025-12-03) + +## [10.0.0-63](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-62...v10.0.0-63) (2025-12-03) + + +### Features + +* add basic health endpoint ([#553](https://github.com/b0ink/doubtfire-deploy/issues/553)) ([e106eeb](https://github.com/b0ink/doubtfire-deploy/commit/e106eebf331bd7f1b12ff1a65fa590d4fba8d9e9)) + +## [10.0.0-62](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-61...v10.0.0-62) (2025-12-03) + + +### Bug Fixes + +* set correct mount ([84b08d8](https://github.com/b0ink/doubtfire-deploy/commit/84b08d8c3e6a18e38585792c873fa174e9bd338a)) + +## [10.0.0-61](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-60...v10.0.0-61) (2025-12-03) + +## [10.0.0-60](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-59...v10.0.0-60) (2025-12-03) + +## [10.0.0-59](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-58...v10.0.0-59) (2025-12-03) + + +### Features + +* discussion prompts ([#546](https://github.com/b0ink/doubtfire-deploy/issues/546)) ([ab38360](https://github.com/b0ink/doubtfire-deploy/commit/ab38360a5c847a1db0d03547fc432b000a61ebb9)) + +## [10.0.0-58](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-57...v10.0.0-58) (2025-11-25) + + +### Bug Fixes + +* allow pdf gen and overseer job to be queued at the same time ([d1d839b](https://github.com/b0ink/doubtfire-deploy/commit/d1d839ba10ee369faf354d758dd1c746bea9e4a9)) + +## [10.0.0-57](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-56...v10.0.0-57) (2025-11-25) + + +### Features + +* env option to skip jplag submission clusters ([#551](https://github.com/b0ink/doubtfire-deploy/issues/551)) ([5e3b4cc](https://github.com/b0ink/doubtfire-deploy/commit/5e3b4cc437fd296519e904a37e53305b579140dd)) + + +### Bug Fixes + +* ensure long comments are appended on a new page [#303](https://github.com/b0ink/doubtfire-deploy/issues/303) ([148cbba](https://github.com/b0ink/doubtfire-deploy/commit/148cbba601020eb41d975bb39f17a72378fc36ad)) + ## [10.0.0-56](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.0-55...v10.0.0-56) (2025-11-10) 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..69690f2e7 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 @@ -102,6 +106,8 @@ class ApiRoot < Grape::API mount WebcalApi mount WebcalPublicApi mount MarkingSessionsApi + mount DiscussionPromptsApi + mount OverseerStepsApi mount Feedback::FeedbackChipApi @@ -118,6 +124,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 @@ -150,13 +157,15 @@ class ApiRoot < Grape::API AuthenticationHelpers.add_auth_to D2lIntegrationApi::D2lApi AuthenticationHelpers.add_auth_to Feedback::FeedbackChipApi AuthenticationHelpers.add_auth_to MarkingSessionsApi + AuthenticationHelpers.add_auth_to DiscussionPromptsApi + AuthenticationHelpers.add_auth_to OverseerStepsApi add_swagger_documentation \ base_path: nil, - api_version: 'v1', + doc_version: 'v10.0.0', hide_documentation_path: true, info: { - title: 'Doubtfire API Documentaion', + title: 'Doubtfire API Documentation', description: 'Doubtfire is a modern, lightweight learning management system.', license: 'AGPL v3.0', license_url: 'https://github.com/doubtfire-lms/doubtfire-api/blob/master/LICENSE' diff --git a/app/api/discussion_prompts_api.rb b/app/api/discussion_prompts_api.rb new file mode 100644 index 000000000..77e9aa498 --- /dev/null +++ b/app/api/discussion_prompts_api.rb @@ -0,0 +1,122 @@ +require 'grape' + +class DiscussionPromptsApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers + + before do + authenticated? + end + + desc "Get all the discussion prompts for a task definition" + params do + requires :task_definition_id, type: Integer, desc: 'Task definition to fetch discussion prompts for' + end + get '/task_definitions/:task_definition_id/discussion_prompts' do + task_definition = TaskDefinition.find(params[:task_definition_id]) + + unless authorise? current_user, task_definition, :get_discussion_prompt + error!({ error: 'You do not have permission to access this project' }, 403) + end + + result = task_definition.discussion_prompts + .order(priority: :desc) + + present result, with: Entities::DiscussionPromptEntity, user: current_user + end + + desc "Create a new discussion prompt for a task definition" + params do + requires :task_definition_id, type: Integer, desc: 'Task definition to fetch discussion prompts for' + requires :content, type: String, desc: 'Discussion prompt content' + requires :priority, type: Integer, desc: 'The priority of the disucssion prompt' + end + post '/task_definitions/:task_definition_id/discussion_prompts' do + task_definition = TaskDefinition.find(params[:task_definition_id]) + + unless authorise? current_user, task_definition, :create_discussion_prompt + error!({ error: 'You do not have permission to access this project' }, 403) + end + + unit = task_definition.unit + + content = params[:content].to_s + priority = params[:priority].to_i + + discussion_prompt = DiscussionPrompt.create!({ + task_definition: task_definition, + content: content, + priority: priority + }) + + present discussion_prompt, with: Entities::DiscussionPromptEntity + end + + desc "Update a discussion prompt for a task definition" + params do + requires :task_definition_id, type: Integer, desc: 'Task definition to fetch discussion prompts for' + requires :content, type: String, desc: 'Discussion prompt content' + requires :priority, type: Integer, desc: 'The priority of the disucssion prompt' + requires :id, type: Integer, desc: 'The ID of the discussion prompt' + end + put '/task_definitions/:task_definition_id/discussion_prompts/:id' do + task_definition = TaskDefinition.find(params[:task_definition_id]) + + unless authorise? current_user, task_definition, :create_discussion_prompt + error!({ error: 'You do not have permission to access this project' }, 403) + end + + discussion_prompt = task_definition.discussion_prompts.find(params[:id]) + + content = params[:content].to_s + priority = params[:priority].to_i + + discussion_prompt.update({ + task_definition: task_definition, + content: content, + priority: priority + }) + end + + desc "Delete a discussion prompt for a task definition" + params do + requires :task_definition_id, type: Integer, desc: 'Task definition to fetch discussion prompts for' + requires :id, type: Integer, desc: 'The ID of the discussion prompt' + end + delete '/task_definitions/:task_definition_id/discussion_prompts/:id' do + task_definition = TaskDefinition.find(params[:task_definition_id]) + + unless authorise? current_user, task_definition, :create_discussion_prompt + error!({ error: 'You do not have permission to access this project' }, 403) + end + + discussion_prompt = task_definition.discussion_prompts.find(params[:id]) + + discussion_prompt.destroy! + present discussion_prompt.destroyed?, with: Grape::Presenters::Presenter + end + + desc "Get all the discussion prompts for a project" + params do + requires :project_id, type: Integer, desc: 'Project to fetch discussion prompts for' + end + get 'projects/:project_id/discussion_prompts' do + project = Project.find(params[:project_id]) + + # TODO: should convenor permissions exist on the project ? + unless authorise? current_user, project, :get_discussion_prompt + error!({ error: 'You do not have permission to access this project' }, 403) + end + + tasks_to_discuss = project.tasks.where(task_status: [TaskStatus.discuss, TaskStatus.attention_required, TaskStatus.demonstrate]) + task_definition_ids = tasks_to_discuss.pluck(:task_definition_id) + + result = DiscussionPrompt + .joins(:task_definition) + .where(task_definition_id: task_definition_ids) + .order('task_definitions.abbreviation ASC, discussion_prompts.priority DESC') + + present result, with: Entities::DiscussionPromptEntity, user: current_user + end + +end diff --git a/app/api/entities/discussion_prompt_entity.rb b/app/api/entities/discussion_prompt_entity.rb new file mode 100644 index 000000000..1ffc160f3 --- /dev/null +++ b/app/api/entities/discussion_prompt_entity.rb @@ -0,0 +1,8 @@ +module Entities + class DiscussionPromptEntity < Grape::Entity + expose :id + expose :task_definition_id + expose :content + expose :priority + end +end diff --git a/app/api/entities/overseer_assessment_entity.rb b/app/api/entities/overseer_assessment_entity.rb index b8402dcf5..2d26ab47c 100644 --- a/app/api/entities/overseer_assessment_entity.rb +++ b/app/api/entities/overseer_assessment_entity.rb @@ -7,5 +7,8 @@ class OverseerAssessmentEntity < Grape::Entity expose :status expose :created_at expose :updated_at + + expose :total_steps + expose :passed_steps end end diff --git a/app/api/entities/overseer_step_entity.rb b/app/api/entities/overseer_step_entity.rb new file mode 100644 index 000000000..b230b577c --- /dev/null +++ b/app/api/entities/overseer_step_entity.rb @@ -0,0 +1,47 @@ +module Entities + class OverseerStepEntity < Grape::Entity + expose :id + expose :task_definition_id + + def staff?(my_role) + Role.teaching_staff_ids.include?(my_role.id) unless my_role.nil? + end + + expose :name, if: ->(_unit, options) { staff?(options[:my_role]) } + expose :description, if: ->(_unit, options) { staff?(options[:my_role]) } + + expose :display_name + expose :display_description + + expose :run_command, if: ->(_unit, options) { staff?(options[:my_role]) } + + expose :timeout, if: ->(_unit, options) { staff?(options[:my_role]) } + expose :sort_order, if: ->(_unit, options) { staff?(options[:my_role]) } + + expose :step_type + expose :partial_output_diff, if: ->(_unit, options) { staff?(options[:my_role]) } + + expose :stdin_input_file, if: ->(_unit, options) { staff?(options[:my_role]) } + expose :expected_output_file, if: ->(_unit, options) { staff?(options[:my_role]) } + + expose :feedback_message, if: ->(_unit, options) { staff?(options[:my_role]) } + + expose :status_on_success, + if: ->(_obj, options) { staff?(options[:my_role]) } do |overseer_step| + TaskStatus.find_by(id: overseer_step.status_on_success_id)&.status_key || 'no_change' + end + + expose :status_on_failure, + if: ->(_obj, options) { staff?(options[:my_role]) } do |overseer_step| + TaskStatus.find_by(id: overseer_step.status_on_failure_id)&.status_key || 'no_change' + end + + expose :halt_on_success, if: ->(_unit, options) { staff?(options[:my_role]) } + expose :halt_on_failure, if: ->(_unit, options) { staff?(options[:my_role]) } + expose :show_expected_output, if: ->(_unit, options) { staff?(options[:my_role]) } + expose :show_stdin, if: ->(_unit, options) { staff?(options[:my_role]) } + expose :show_stdout, if: ->(_unit, options) { staff?(options[:my_role]) } + + expose :enabled + end +end diff --git a/app/api/entities/overseer_step_result_entity.rb b/app/api/entities/overseer_step_result_entity.rb new file mode 100644 index 000000000..eeebd0caa --- /dev/null +++ b/app/api/entities/overseer_step_result_entity.rb @@ -0,0 +1,30 @@ +module Entities + class OverseerStepResultEntity < Grape::Entity + + def staff?(my_role) + Role.teaching_staff_ids.include?(my_role.id) unless my_role.nil? + end + + expose :id + expose :overseer_step_id + expose :exit_status + expose :pass + expose :feedback_message + + expose :stdout, if: lambda { |result, options| + staff?(options[:my_role]) || result.overseer_step&.show_stdout + } + + expose :stdin, if: lambda { |result, options| + staff?(options[:my_role]) || result.overseer_step&.show_stdin + } + + expose :expected_output, if: lambda { |result, options| + staff?(options[:my_role]) || result.overseer_step&.show_expected_output + } + + expose :stdout_sha256 + expose :stdin_sha256 + expose :expected_output_sha256 + end +end diff --git a/app/api/entities/task_definition_entity.rb b/app/api/entities/task_definition_entity.rb index faa8d3752..fda5e31ce 100644 --- a/app/api/entities/task_definition_entity.rb +++ b/app/api/entities/task_definition_entity.rb @@ -50,12 +50,24 @@ def staff?(my_role) expose :is_graded expose :max_quality_pts expose :overseer_image_id, if: ->(unit, options) { staff?(options[:my_role]) }, expose_nil: false - expose :assessment_enabled, if: ->(unit, options) { staff?(options[:my_role]) } + # expose :assessment_enabled, if: ->(unit, options) { staff?(options[:my_role]) } + expose :assessment_enabled expose :similarity_language, if: ->(unit, options) { staff?(options[:my_role]) }, expose_nil: false expose :assess_in_portfolio_only expose :use_resources_for_jplag_base_code, if: ->(unit, options) { staff?(options[:my_role]) } expose :lock_assessments_to_tutorial_stream, if: ->(unit, options) { staff?(options[:my_role]) } expose :learning_outcomes, using: LearningOutcomeEntity, as: :ilos + + expose :discussion_prompts_count do |task_def| + task_def.discussion_prompts.size + end + + # expose :overseer_steps, using: OverseerStepEntity, if: ->(unit, options) { staff?(options[:my_role]) } + expose :overseer_steps, using: OverseerStepEntity do |task_def, options| + task_def.overseer_steps # options[:my_role] is still available inside the entity + end + expose :overseer_resource_files, if: ->(task_def, options) { staff?(options[:my_role]) } + end end diff --git a/app/api/entities/task_entity.rb b/app/api/entities/task_entity.rb index ffb53bd86..1de8fb6b3 100644 --- a/app/api/entities/task_entity.rb +++ b/app/api/entities/task_entity.rb @@ -14,6 +14,8 @@ class TaskEntity < Grape::Entity expose :due_date expose :submission_date, expose_nil: false expose :completion_date, expose_nil: false + expose :target_due_date, expose_nil: false + expose :target_start_date, expose_nil: false end expose :extensions diff --git a/app/api/extension_comments_api.rb b/app/api/extension_comments_api.rb index 1d510f079..86acd33f2 100644 --- a/app/api/extension_comments_api.rb +++ b/app/api/extension_comments_api.rb @@ -10,13 +10,20 @@ 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) + # 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] + ) - # 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) + # Handle the service response + if result[:success] + present result[:result].serialize(current_user), Grape::Presenters::Presenter + else + error!({ error: result[:error] }, result[:status]) end if project.unit.allow_flexible_dates 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/overseer_steps_api.rb b/app/api/overseer_steps_api.rb new file mode 100644 index 000000000..9b695b6dd --- /dev/null +++ b/app/api/overseer_steps_api.rb @@ -0,0 +1,200 @@ +require 'grape' + +class OverseerStepsApi < Grape::API + helpers AuthenticationHelpers + helpers AuthorisationHelpers + helpers SidekiqHelper + + before do + authenticated? + end + + desc 'Add an overseer step' + params do + requires :overseer_step, type: Hash do + requires :name, type: String + optional :description, type: String + optional :display_name, type: String + optional :display_description, type: String + optional :run_command, type: String + optional :timeout, type: Integer + # TODO: rename to execution_order || exec_order? + optional :sort_order, type: Integer + optional :partial_output_diff, type: Boolean + requires :step_type, type: String + optional :stdin_input_file, type: String + optional :expected_output_file, type: String + optional :feedback_message, type: String + optional :status_on_success, type: String + optional :status_on_failure, type: String + optional :halt_on_success, type: Boolean + optional :halt_on_failure, type: Boolean + optional :show_expected_output, type: Boolean + optional :show_stdin, type: Boolean + optional :show_stdout, type: Boolean + optional :enabled, type: Boolean + end + requires :task_def_id, type: Integer + end + post '/units/:unit_id/task_definitions/:task_def_id/overseer_steps' do + unless Doubtfire::Application.config.overseer_enabled + error!({ error: 'Overseer is not enabled. Enable Overseer before updating settings.' }, 403) + end + + task_definition = TaskDefinition.find(params[:task_def_id]) + + unless authorise? current_user, task_definition, :manage_overseer_steps + error!({ error: 'Not authorised to manage overseer for this task definition' }, 403) + end + + status_on_success_param = params[:overseer_step][:status_on_success] + status_on_failure_param = params[:overseer_step][:status_on_failure] + + status_on_success_id = status_on_success_param.present? && status_on_success_param != 'no_change' ? TaskStatus.status_for_name(status_on_success_param)&.id : nil + status_on_failure_id = status_on_failure_param.present? && status_on_failure_param != 'no_change' ? TaskStatus.status_for_name(status_on_failure_param)&.id : nil + + overseer_step_params = ActionController::Parameters.new(params) + .require(:overseer_step) + .permit( + :name, + :description, + :display_name, + :display_description, + :run_command, + :timeout, + :sort_order, + :step_type, + :partial_output_diff, + :stdin_input_file, + :expected_output_file, + :feedback_message, + :status_on_success_id, + :status_on_failure_id, + :halt_on_success, + :halt_on_failure, + :show_expected_output, + :show_stdin, + :show_stdout, + :enabled + ) + .merge(task_definition_id: task_definition.id, + status_on_success_id: status_on_success_id, + status_on_failure_id: status_on_failure_id) + + result = OverseerStep.create!(overseer_step_params) + + if result.nil? + error!({ error: 'No overseer step added' }, 403) + else + present result, with: Entities::OverseerStepEntity + end + end + + desc 'Update an overseer step' + params do + requires :overseer_step, type: Hash do + optional :name, type: String + optional :description, type: String + optional :display_name, type: String + optional :display_description, type: String + optional :run_command, type: String + optional :timeout, type: Integer + optional :sort_order, type: Integer + optional :step_type, type: String + optional :partial_output_diff, type: Boolean + optional :stdin_input_file, type: String + optional :expected_output_file, type: String + optional :feedback_message, type: String + optional :status_on_success, type: String + optional :status_on_failure, type: String + optional :halt_on_success, type: Boolean + optional :halt_on_failure, type: Boolean + optional :show_expected_output, type: Boolean + optional :show_stdin, type: Boolean + optional :show_stdout, type: Boolean + optional :enabled, type: Boolean + end + requires :task_def_id, type: Integer + end + put '/units/:unit_id/task_definitions/:task_def_id/overseer_steps/:id' do + unless Doubtfire::Application.config.overseer_enabled + error!({ error: 'Overseer is not enabled. Enable Overseer before updating settings.' }, 403) + end + + unit = Unit.find(params[:unit_id]) + task_definition = unit.task_definitions.find(params[:task_def_id]) + overseer_step = task_definition.overseer_steps.find(params[:id]) + + unless authorise? current_user, overseer_step.task_definition, :manage_overseer_steps + error!({ error: 'Not authorised to manage overseer for this task definition' }, 403) + end + + status_on_success_param = params[:overseer_step][:status_on_success] + status_on_failure_param = params[:overseer_step][:status_on_failure] + + status_on_success_id = status_on_success_param.present? && status_on_success_param != 'no_change' ? TaskStatus.status_for_name(status_on_success_param)&.id : nil + status_on_failure_id = status_on_failure_param.present? && status_on_failure_param != 'no_change' ? TaskStatus.status_for_name(status_on_failure_param)&.id : nil + + overseer_step_params = ActionController::Parameters.new(params) + .require(:overseer_step) + .permit( + :name, + :description, + :display_name, + :display_description, + :run_command, + :timeout, + :sort_order, + :step_type, + :partial_output_diff, + :stdin_input_file, + :expected_output_file, + :feedback_message, + :status_on_success_id, + :status_on_failure_id, + :halt_on_success, + :halt_on_failure, + :show_expected_output, + :show_stdin, + :show_stdout, + :enabled + ) + .merge( + status_on_success_id: status_on_success_id, + status_on_failure_id: status_on_failure_id + ) + + overseer_step.update!(overseer_step_params) + + present overseer_step, with: Entities::OverseerStepEntity + end + + desc 'Delete an overseer step' + delete '/overseer_steps/:id' do + overseer_step = OverseerStep.find(params[:id]) + + unless authorise? current_user, overseer_step.task_definition, :manage_overseer_steps + error!({ error: 'Not authorised to manage overseer for this task definition' }, 403) + end + + overseer_step.destroy! + + error!({ error: overseer_step.errors.full_messages.last }, 403) unless overseer_step.destroyed? + + present overseer_step.destroyed?, with: Grape::Presenters::Presenter + end + + desc 'Get test results for an overseer assessment' + get '/projects/:project_id/task_definitions/:task_def_id/overseer_assessments_results/:id' do + project = Project.find(params[:project_id]) + + unless authorise? current_user, project, :get_submission + error!({ error: 'Not authorised to view this project' }, 403) + end + + unit = project.unit + + overseer_assessment = OverseerAssessment.find(params[:id]) + present overseer_assessment.overseer_step_results, with: Entities::OverseerStepResultEntity, my_role: unit.role_for(current_user) + 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..a54ac214a --- /dev/null +++ b/app/api/staff_grant_extension_api.rb @@ -0,0 +1,181 @@ +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], + true # 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/api/submission/portfolio_evidence_api.rb b/app/api/submission/portfolio_evidence_api.rb index 2316277be..cf17a4256 100644 --- a/app/api/submission/portfolio_evidence_api.rb +++ b/app/api/submission/portfolio_evidence_api.rb @@ -6,6 +6,8 @@ class PortfolioEvidenceApi < Grape::API helpers AuthenticationHelpers helpers AuthorisationHelpers helpers FileStreamHelper + helpers Base64Helper + include LogHelper def self.logger @@ -20,6 +22,7 @@ def self.logger ready_for_feedback: 1, assess_in_portfolio: 1, discuss: 2, + attention_required: 0, demonstrate: 2, complete: 3 }.freeze @@ -98,18 +101,6 @@ def self.logger # Copy files to be PDFed task.accept_submission(current_user, scoop_files(params, upload_reqs), self, params[:contributions], trigger, alignments, accepted_tii_eula: params[:accepted_tii_eula]) - if task.overseer_enabled? - overseer_assessment = OverseerAssessment.create_for(task) - if overseer_assessment.present? - logger.info "Launching Overseer assessment for task_def_id: #{task_definition.id} task_id: #{task.id}" - - overseer_assessment.send_to_overseer - - - else - logger.info "Overseer assessment for task_def_id: #{task_definition.id} task_id: #{task.id} was not performed #{overseer_assessment.inspect}" - end - end present task, with: Entities::TaskEntity, update_only: true end @@ -244,9 +235,16 @@ def self.logger end result = [] - yaml_data = YAML.load_file("#{path}/output.yaml") # returns a hash + begin + yaml_data = YAML.load_file("#{path}/output.yaml") # returns a hash + rescue Psych::SyntaxError => e + error!({ error: "Failed to parse overseer output: #{e.message}" }, 401) + end yaml_data.each do |key, value| + if base64?(value) + value = Base64.decode64(value) + end result << { label: key, result: value } end @@ -292,9 +290,16 @@ def self.logger end result = [] - yaml_data = YAML.load_file("#{path}/output.yaml") # returns a hash + begin + yaml_data = YAML.load_file("#{path}/output.yaml") # returns a hash + rescue Psych::SyntaxError => e + error!({ error: "Failed to parse overseer output: #{e.message}" }, 401) + end yaml_data.each do |key, value| + if base64?(value) + value = Base64.decode64(value) + end result << { label: key, result: value } end diff --git a/app/api/task_definitions_api.rb b/app/api/task_definitions_api.rb index c5e031279..b4fd9a127 100644 --- a/app/api/task_definitions_api.rb +++ b/app/api/task_definitions_api.rb @@ -335,17 +335,17 @@ class TaskDefinitionsApi < Grape::API upload_reqs = task.upload_requirements # Copy files to be PDFed - task.accept_submission(current_user, scoop_files(params, upload_reqs), self, nil, 'ready_for_feedback', nil, accepted_tii_eula: false) + task.accept_submission(current_user, scoop_files(params, upload_reqs), self, nil, 'ready_for_feedback', nil, accepted_tii_eula: false, test_submission: true) - logger.info '********* - about to perform overseer submission' - overseer_assessment = OverseerAssessment.create_for(task) - if overseer_assessment.present? - overseer_assessment.send_to_overseer + # logger.info '********* - about to perform overseer submission' + # overseer_assessment = OverseerAssessment.create_for(task) + # if overseer_assessment.present? + # overseer_assessment.send_to_overseer - logger.info "Overseer assessment for task_def_id: #{task_definition.id} task_id: #{task.id} was performed" - else - logger.info "Overseer assessment for task_def_id: #{task_definition.id} task_id: #{task.id} was not performed" - end + # logger.info "Overseer assessment for task_def_id: #{task_definition.id} task_id: #{task.id} was performed" + # else + # logger.info "Overseer assessment for task_def_id: #{task_definition.id} task_id: #{task.id} was not performed" + # end # todo: Do we need to return additional details here? e.g. the comment, and project? present task, with: Entities::TaskEntity, include_other_projects: true, update_only: true @@ -880,45 +880,45 @@ class TaskDefinitionsApi < Grape::API present job, with: Entities::SidekiqJobEntity end - desc 'Retrieve the contents of the overseer execution script' - params do - requires :unit_id, type: Integer, desc: 'The unit that has the task definition' - requires :task_def_id, type: Integer, desc: 'The task definition to download submissions for' - end - get '/units/:unit_id/task_definitions/:task_def_id/overseer_script' do - unit = Unit.find(params[:unit_id]) - unless authorise? current_user, unit, :add_task_def - error!({ error: 'Not authorised to edit task details of unit' }, 403) - end + # desc 'Retrieve the contents of the overseer execution script' + # params do + # requires :unit_id, type: Integer, desc: 'The unit that has the task definition' + # requires :task_def_id, type: Integer, desc: 'The task definition to download submissions for' + # end + # get '/units/:unit_id/task_definitions/:task_def_id/overseer_script' do + # unit = Unit.find(params[:unit_id]) + # unless authorise? current_user, unit, :add_task_def + # error!({ error: 'Not authorised to edit task details of unit' }, 403) + # end - td = unit.task_definitions.find(params[:task_def_id]) + # td = unit.task_definitions.find(params[:task_def_id]) - script_path = td.task_assessment_script + # script_path = td.task_assessment_script - content = File.read(script_path) - content - end + # content = File.read(script_path) + # content + # end - desc 'Update the contents of the overseer execution script' - params do - requires :unit_id, type: Integer, desc: 'The unit that has the task definition' - requires :task_def_id, type: Integer, desc: 'The task definition to download submissions for' - requires :script_content, type: String, desc: 'Content of the overseer execution script' - end - put '/units/:unit_id/task_definitions/:task_def_id/overseer_script' do - unit = Unit.find(params[:unit_id]) - unless authorise? current_user, unit, :add_task_def - error!({ error: 'Not authorised to edit task details of unit' }, 403) - end + # desc 'Update the contents of the overseer execution script' + # params do + # requires :unit_id, type: Integer, desc: 'The unit that has the task definition' + # requires :task_def_id, type: Integer, desc: 'The task definition to download submissions for' + # requires :script_content, type: String, desc: 'Content of the overseer execution script' + # end + # put '/units/:unit_id/task_definitions/:task_def_id/overseer_script' do + # unit = Unit.find(params[:unit_id]) + # unless authorise? current_user, unit, :add_task_def + # error!({ error: 'Not authorised to edit task details of unit' }, 403) + # end - td = unit.task_definitions.find(params[:task_def_id]) + # td = unit.task_definitions.find(params[:task_def_id]) - script_path = td.task_assessment_script + # script_path = td.task_assessment_script - decoded = Base64.urlsafe_decode64(params[:script_content]) + # decoded = Base64.urlsafe_decode64(params[:script_content]) - File.write(script_path, decoded) - status 200 - end + # File.write(script_path, decoded) + # status 200 + # end end diff --git a/app/api/task_prerequisites_api.rb b/app/api/task_prerequisites_api.rb index 04e190fd6..3cca5d2ff 100644 --- a/app/api/task_prerequisites_api.rb +++ b/app/api/task_prerequisites_api.rb @@ -26,4 +26,19 @@ class TaskPrerequisitesApi < Grape::API present prerequisites, with: Entities::TaskPrerequisiteEntity end + desc 'Get task prerequisites for a unit' + params do + requires :unit_id, type: Integer, desc: 'The unit to get the task definition from' + end + get '/units/:unit_id/task_prerequisites' do + unit = Unit.find(params[:unit_id]) + + unless authorise? current_user, unit, :get_unit + error!({ error: 'Not authorised to get unit' }, 403) + end + + prerequisites = unit.task_definitions.flat_map(&:task_prerequisites) + + present prerequisites, with: Entities::TaskPrerequisiteEntity + end end diff --git a/app/api/tasks_api.rb b/app/api/tasks_api.rb index 4929e3903..47a41e64b 100644 --- a/app/api/tasks_api.rb +++ b/app/api/tasks_api.rb @@ -330,4 +330,98 @@ class TasksApi < Grape::API # Return the file data stream_file file_loc end + + desc 'Update the target dates for a task - when date flexibility is allowed' + params do + requires :id, type: Integer, desc: 'The project id to locate' + requires :task_definition_id, type: Integer, desc: 'The id of the task definition of the task to update in this project' + requires :target_start_date, type: Date, desc: 'Target date to start the task' + requires :target_due_date, type: Date, desc: 'Target date to submit the task' + end + put '/projects/:id/task_def_id/:task_definition_id/target_dates' do + project = Project.find(params[:id]) + task_definition = project.unit.task_definitions.find(params[:task_definition_id]) + + # check the user can put this task + if authorise? current_user, project, :make_submission + # Check unit allows planned date changes + unless project.unit.allow_flexible_dates + error!({ error: 'This unit does not allow you to adjust due dates.' }, 403) + end + + task = project.task_for_task_definition(task_definition) + + if task.target_start_date == params[:target_start_date] && task.target_due_date == params[:target_due_date] + present task, with: Entities::TaskEntity, include_other_projects: true, update_only: true + return + end + + task.update!( + target_start_date: params[:target_start_date], + target_due_date: params[:target_due_date] + ) + + comment_text = if params[:target_start_date].present? && params[:target_due_date].present? + "Planned date adjusted: #{task.target_start_date.strftime('%d %b')} - #{task.target_due_date.strftime('%d %b')}." + else + "Planned date reset: #{task_definition.start_date.strftime('%d %b')} - #{task_definition.target_date.strftime('%d %b')}." + end + + comment = TaskComment.create( + task: task, + user: current_user, + comment: comment_text, + content_type: :plan, + recipient: project.student + ) + + comment.mark_as_read(project.tutor_for(task_definition)) + + present task, with: Entities::TaskEntity, include_other_projects: true, update_only: true + else + error!({ error: "You are not permitted to adjust the plan." }, 403) + end + end + + desc 'Update the target dates for a task - when date flexibility is allowed' + params do + requires :id, type: Integer, desc: 'The project id to locate' + end + put '/projects/:id/reset_target_dates' do + project = Project.find(params[:id]) + + # check the user can put this task + if authorise? current_user, project, :make_submission + # Check unit allows planned date changes + unless project.unit.allow_flexible_dates + error!({ error: 'This unit does not allow you to adjust due dates.' }, 403) + end + + project.tasks.each do |task| + next if task.target_start_date.nil? && task.target_due_date.nil? + + task.update!( + target_start_date: nil, + target_due_date: nil + ) + + comment_text = "Planned date reset: #{task.task_definition.start_date.strftime('%d %b')} - #{task.task_definition.target_date.strftime('%d %b')}." + comment = TaskComment.create( + task: task, + user: current_user, + comment: comment_text, + content_type: :plan, + recipient: project.student + ) + + comment.mark_as_read(project.tutor_for(task.task_definition)) + end + + present project, with: Entities::ProjectEntity, user: current_user, for_student: true, in_project: true + + else + error!({ error: "You are not permitted to adjust the plan." }, 403) + end + end + end diff --git a/app/api/unit_roles_api.rb b/app/api/unit_roles_api.rb index 85ead4295..57f8ba3f2 100644 --- a/app/api/unit_roles_api.rb +++ b/app/api/unit_roles_api.rb @@ -87,7 +87,11 @@ class UnitRolesApi < Grape::API # Once they're an observer, they'll no longer have access to this route to remove the observer status from themselves # But let's double check just in case this route gets whitelisted... - if unit_role.observer_only + + unit = unit_role.unit + current_unit_role = unit.unit_role_for(current_user) + + if current_unit_role.observer_only error!({ error: "You are not authorised to update this staff member." }, 403) end diff --git a/app/helpers/authorisation_helpers.rb b/app/helpers/authorisation_helpers.rb index 6ebc7605d..b27fd5902 100644 --- a/app/helpers/authorisation_helpers.rb +++ b/app/helpers/authorisation_helpers.rb @@ -25,7 +25,8 @@ def get_permission_hash(role, perm_hash, _other) :get_discussion, :get_staff_note, :get_members, - :get_groups + :get_groups, + :get_discussion_prompt ].freeze # diff --git a/app/helpers/base64_helper.rb b/app/helpers/base64_helper.rb new file mode 100644 index 000000000..8222ce6f5 --- /dev/null +++ b/app/helpers/base64_helper.rb @@ -0,0 +1,7 @@ +module Base64Helper + def base64?(value) + value.is_a?(String) && Base64.strict_encode64(Base64.decode64(value)) == value + rescue ArgumentError + false + end +end diff --git a/app/mailers/notifications_mailer.rb b/app/mailers/notifications_mailer.rb index 433f2e7eb..5c810fd80 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 @@ -61,7 +71,7 @@ def weekly_student_summary(project, summary_stats, did_revert_to_pass) @student_engagements = @engagements.select { |e| [TaskStatus.not_started.name, TaskStatus.need_help.name, TaskStatus.working_on_it.name, TaskStatus.ready_for_feedback.name].include? e.engagement }.count - @staff_engagements = @engagements.select { |e| [TaskStatus.complete.name, TaskStatus.feedback_exceeded.name, TaskStatus.redo.name, TaskStatus.discuss.name, TaskStatus.demonstrate.name, TaskStatus.fail.name].include? e.engagement }.count + @staff_engagements = @engagements.select { |e| [TaskStatus.complete.name, TaskStatus.feedback_exceeded.name, TaskStatus.redo.name, TaskStatus.discuss.name, TaskStatus.attention_required.name, TaskStatus.demonstrate.name, TaskStatus.fail.name].include? e.engagement }.count @task_states = project.tasks.joins(:task_status).select("count(tasks.id) as number, task_statuses.name as status").group("task_statuses.name") @@ -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/comments/assessment_comment.rb b/app/models/comments/assessment_comment.rb index 5c281d32c..1b614b41f 100644 --- a/app/models/comments/assessment_comment.rb +++ b/app/models/comments/assessment_comment.rb @@ -6,6 +6,9 @@ class AssessmentComment < TaskComment def serialize(user) json = super(user) json[:overseer_assessment_id] = self.commentable_id + json[:overseer_total_steps] = self.commentable.total_steps + json[:overseer_passed_steps] = self.commentable.passed_steps + json[:overseer_status] = self.commentable.status json end end diff --git a/app/models/discussion_prompt.rb b/app/models/discussion_prompt.rb new file mode 100644 index 000000000..7bbd14c80 --- /dev/null +++ b/app/models/discussion_prompt.rb @@ -0,0 +1,7 @@ +class DiscussionPrompt < ApplicationRecord + + belongs_to :task_definition, optional: false + belongs_to :project, optional: true + belongs_to :created_by, class_name: 'User', optional: true + +end 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..5fd5fe80c 100644 --- a/app/models/overseer_assessment.rb +++ b/app/models/overseer_assessment.rb @@ -4,6 +4,7 @@ class OverseerAssessment < ApplicationRecord has_one :project, through: :task has_many :assessment_comments, as: :commentable, dependent: :destroy + has_many :overseer_step_results, dependent: :destroy validates :status, presence: true validates :task_id, presence: true @@ -11,12 +12,15 @@ class OverseerAssessment < ApplicationRecord validates :submission_timestamp, uniqueness: { scope: :task_id } - enum :status, { pre_queued: 0, queued: 1, queue_failed: 2, done: 3 } + enum :status, { pre_queued: 0, passed: 1, failed: 2 } after_destroy :delete_associated_files + # TODO: track how many tests ran, and how many tests total at the time + # TODO: we might not have an overseerStepResult because a new test was added later + # Creates an OverseerAssessment object for a new submission - def self.create_for(task) + def self.create_for(task, test_submission) # Create only if: # unit's assessment is enabled && # task's assessment is enabled && @@ -26,7 +30,7 @@ def self.create_for(task) task_definition = task.task_definition unit = task_definition.unit - return nil unless task.overseer_enabled? + return nil unless task.overseer_enabled? || test_submission docker_image_name_tag = task_definition.docker_image_name_tag || unit.docker_image_name_tag # assessment_resources_path = task_definition.task_assessment_resources @@ -118,7 +122,7 @@ def update_assessment_comment(text) add_assessment_comment text end - def send_to_overseer() + def send_to_overseer(test_submission: false) 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 @@ -140,10 +144,10 @@ def send_to_overseer() assessment_resources_path = task_definition.task_assessment_resources - unless unit.assessment_enabled && - task_definition.assessment_enabled && - task_definition.has_task_assessment_script? && - (task.has_new_files? || task.has_done_file?) + unless unit.assessment_enabled && + (task_definition.assessment_enabled || test_submission) && + # task_definition.has_task_assessment_script? && + (task.has_new_files? || task.has_done_file?) puts "ERROR: Assessment is no longer configured for overseer assessment. Unable to send - OverseerAssessment #{id}" return { error: "This assessment is no longer setup for automated feedback. Automated feedback is turned off at either the unit or task level, or the task does not have the scripts needed to automate assessment." } @@ -199,12 +203,22 @@ def update_from_output(work_dir_path) comment_txt = '' if !yaml_file['build_message'].nil? && !yaml_file['build_message'].strip.empty? comment_txt += "Build output:\n" - comment_txt += yaml_file['build_message'] + comment_txt += if base64?(yaml_file['run_message']) + Base64.urlsafe_decode64(yaml_file['build_message']) + else + yaml_file['run_message'] + end + comment_txt += "\n" end if !yaml_file['run_message'].nil? && !yaml_file['run_message'].strip.empty? comment_txt += "\n" unless comment_txt.empty? comment_txt += "Execution output:\n" - comment_txt += yaml_file['run_message'] + comment_txt += if base64?(yaml_file['run_message']) + Base64.urlsafe_decode64(yaml_file['run_message']) + else + yaml_file['run_message'] + end + comment_txt += "\n" end if !yaml_file['message'].nil? && !yaml_file['message'].strip.empty? @@ -214,7 +228,7 @@ def update_from_output(work_dir_path) end if comment_txt.present? - update_assessment_comment(comment_txt) + update_assessment_comment(comment_txt[0, 4000]) # Truncate to 4000 characters else puts 'YAML file doesn\'t contain field `build_message` or `run_message`' end @@ -245,5 +259,14 @@ def update_from_output(work_dir_path) def delete_associated_files FileUtils.rm_rf output_path end + + def base64?(value) + value.is_a?(String) && Base64.strict_encode64(Base64.decode64(value)) == value + end + + + def passed_steps + overseer_step_results.select(&:pass).size + end end # rubocop:enable Rails/Output diff --git a/app/models/overseer_step.rb b/app/models/overseer_step.rb new file mode 100644 index 000000000..6273f0db0 --- /dev/null +++ b/app/models/overseer_step.rb @@ -0,0 +1,6 @@ +class OverseerStep < ApplicationRecord + belongs_to :task_definition, optional: false + + validates :timeout, presence: true, numericality: { greater_than_or_equal_to: 1, less_than_or_equal_to: 300, message: 'must be between 1 and 300' } + validates :sort_order, presence: true, numericality: { greater_than_or_equal_to: 0 } +end diff --git a/app/models/overseer_step_result.rb b/app/models/overseer_step_result.rb new file mode 100644 index 000000000..21e482158 --- /dev/null +++ b/app/models/overseer_step_result.rb @@ -0,0 +1,5 @@ +class OverseerStepResult < ApplicationRecord + belongs_to :overseer_assessment, optional: false + belongs_to :overseer_step, optional: false + +end diff --git a/app/models/project.rb b/app/models/project.rb index 578b0dfbf..1e5782484 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -73,14 +73,16 @@ def self.permissions :change_campus, :get_staff_note, :create_staff_note, - :reprocess_submission + :reprocess_submission, + :get_discussion_prompt ] # What can admins do with projects? admin_role_permissions = [ :get, :get_submission, - :reprocess_submission + :reprocess_submission, + :get_discussion_prompt ] # What can auditors do with projects? @@ -88,7 +90,8 @@ def self.permissions :get, :get_submission, :get_staff_note, - :reprocess_submission + :reprocess_submission, + :get_discussion_prompt ] # What can nil users do with projects? @@ -307,7 +310,9 @@ def task_details_for_shallow_serializer(user) scorm_extensions: t.scorm_extensions, due_date: t.due_date, submission_date: t.submission_date, - completion_date: t.completion_date + completion_date: t.completion_date, + target_start_date: t.target_start_date, + target_due_date: t.target_due_date } end end diff --git a/app/models/similarity/unit_similarity_module.rb b/app/models/similarity/unit_similarity_module.rb index 64cc5f0f2..e5f3144e7 100644 --- a/app/models/similarity/unit_similarity_module.rb +++ b/app/models/similarity/unit_similarity_module.rb @@ -302,8 +302,11 @@ def run_jplag_on_done_files(task_definition, tasks_dir, tasks_with_files, unit_c base_code_string = use_base_code ? "--base-code=#{tasks_dir_split}/base" : "" + skip_cluster_check = Doubtfire::Application.config.jplag_skip_cluster_check + skip_cluster_string = skip_cluster_check ? '--cluster-skip' : '' + # Run JPLAG on the extracted files. JPlag container should already be in the /jplag/ workdir. - docker_command = "docker exec -i jplag java -jar jplag-jar-with-dependencies.jar #{tasks_dir_split}/submissions #{base_code_string} -l #{file_lang} --similarity-threshold=#{similarity_threshold} #{min_token_string} -M RUN -r #{results_dir}/#{task_definition.abbreviation}-result --overwrite" + docker_command = "docker exec -i jplag java -jar jplag-jar-with-dependencies.jar #{tasks_dir_split}/submissions #{base_code_string} -l #{file_lang} --similarity-threshold=#{similarity_threshold} #{min_token_string} #{skip_cluster_string} -M RUN -r #{results_dir}/#{task_definition.abbreviation}-result --overwrite" logger.debug "Executing command: #{docker_command}" system(docker_command) diff --git a/app/models/task.rb b/app/models/task.rb index 1c11c8f52..e6a166fc6 100644 --- a/app/models/task.rb +++ b/app/models/task.rb @@ -542,7 +542,7 @@ def trigger_transition(trigger: '', by_user: nil, bulk: false, group_transition: # Can only be graded if task_def is not assess_in_portfolio_only if task_definition.max_quality_pts > 0 case status - when TaskStatus.complete, TaskStatus.discuss, TaskStatus.demonstrate + when TaskStatus.complete, TaskStatus.discuss, TaskStatus.demonstrate, TaskStatus.attention_required update(quality_pts: quality) end end @@ -1403,7 +1403,7 @@ def create_submission_and_trigger_state_change(user, propagate = true, contribut # # Checks to make sure that the files match what we expect # - def accept_submission(current_user, files, ui, contributions, trigger, alignments, accepted_tii_eula: false) + def accept_submission(current_user, files, ui, contributions, trigger, alignments, accepted_tii_eula: false, test_submission: false) # Ensure there is not a submission already in process if processing_pdf? ui.error!({ 'error' => 'A submission is already being processed. Please wait for the current submission process to complete.' }, 403) @@ -1502,7 +1502,7 @@ def accept_submission(current_user, files, ui, contributions, trigger, alignment logger.info "Submission accepted! Status for task #{id} is now #{trigger}" # Trigger processing of new submission - async - AcceptSubmissionJob.perform_async(id, current_user.id, accepted_tii_eula) + AcceptSubmissionJob.perform_async(id, current_user.id, accepted_tii_eula, test_submission) end # The name that should be used for the uploaded file (based on index of upload requirements) @@ -1573,7 +1573,7 @@ def archive_submission def overseer_enabled? return unit.assessment_enabled && task_definition.assessment_enabled && - task_definition.has_task_assessment_script? && + # task_definition.has_task_assessment_script? && (has_new_files? || has_done_file?) end diff --git a/app/models/task_definition.rb b/app/models/task_definition.rb index 27f08f699..7902ca942 100644 --- a/app/models/task_definition.rb +++ b/app/models/task_definition.rb @@ -13,7 +13,10 @@ def self.permissions :update, :upload_csv, :get_los, - :create_task_prerequisite + :create_task_prerequisite, + :get_discussion_prompt, + :create_discussion_prompt, + :manage_overseer_steps ] admin_role_permissions = [ @@ -22,12 +25,17 @@ def self.permissions :update, :upload_csv, :get_los, - :create_task_prerequisite + :create_task_prerequisite, + :get_discussion_prompt, + :create_discussion_prompt, + :manage_overseer_steps ] tutor_role_permissions = [ :get_feedback_chips, - :get_los + :get_los, + :get_discussion_prompt, + :create_discussion_prompt ] auditor_role_permissions = [ @@ -65,6 +73,7 @@ def self.permissions has_many :tasks, dependent: :destroy # Destroying a task definition will also nuke any instances has_many :group_submissions, dependent: :destroy # Destroying a task definition will also nuke any group submissions has_many :learning_outcomes, as: :context, dependent: :destroy + has_many :overseer_steps, -> { order(:sort_order) }, inverse_of: :task_definition, dependent: :destroy has_many :tii_group_attachments, dependent: :destroy # destroy uploaded files to tii - after the tasks has_many :tii_actions, as: :entity, dependent: :destroy @@ -72,6 +81,8 @@ def self.permissions has_many :task_prerequisites, dependent: :destroy has_many :prerequisites, through: :task_prerequisites, source: :prerequisite + has_many :discussion_prompts, dependent: :destroy + serialize :upload_requirements, coder: JSON # Model validations/constraints @@ -294,7 +305,7 @@ def check_upload_requirements_format end # Check the name matches a valid filename format - unless req['name'].match?(/^[a-zA-Z0-9_\- \.]+$/) + unless req['name'].match?(/^[a-zA-Z0-9_\- .]+$/) errors.add(:upload_requirements, "the name for item #{i + 1} does not seem to be a valid filename --> #{req['name']}.") end @@ -463,7 +474,7 @@ def propogate_date_changes date_diff def to_csv_row TaskDefinition.csv_columns - .reject { |col| [:start_week, :start_day, :target_week, :target_day, :due_week, :due_day, :upload_requirements, :group_set, :tutorial_stream, :assess_in_portfolio_only, :task_prerequisites].include? col } + .reject { |col| [:start_week, :start_day, :target_week, :target_day, :due_week, :due_day, :upload_requirements, :group_set, :tutorial_stream, :assess_in_portfolio_only, :task_prerequisites, :discussion_prompts].include? col} .map { |column| attributes[column.to_s] } + [ group_set.nil? ? "" : group_set.name, @@ -482,6 +493,12 @@ def to_csv_row abbreviation: prereq.abbreviation, task_status_id: tp.task_status_id } + end.to_json, + discussion_prompts.map do |prompt| + { + content: prompt.content, + priority: prompt.priority + } end.to_json ] # [target_date.strftime('%d-%m-%Y')] + @@ -492,7 +509,7 @@ def self.csv_columns [:name, :abbreviation, :description, :weighting, :target_grade, :restrict_status_updates, :max_quality_pts, :is_graded, :plagiarism_warn_pct, :scorm_enabled, :scorm_allow_review, :scorm_bypass_test, :scorm_time_delay_enabled, :scorm_attempt_limit, :group_set, :upload_requirements, :start_week, :start_day, :target_week, :target_day, - :due_week, :due_day, :tutorial_stream, :assess_in_portfolio_only, :task_prerequisites] + :due_week, :due_day, :tutorial_stream, :assess_in_portfolio_only, :task_prerequisites, :discussion_prompts] end def self.task_def_for_csv_row(unit, row) @@ -554,6 +571,20 @@ def self.task_def_for_csv_row(unit, row) result.tutorial_stream = unit.tutorial_streams.where(abbreviation: row[:tutorial_stream]).first end + result.discussion_prompts.destroy_all + + if row[:discussion_prompts].present? + prompts = JSON.parse(row[:discussion_prompts]) + prompts.each do |prompt| + DiscussionPrompt.create!({ + task_definition: result, + content: prompt['content'], + priority: prompt['priority'] + }) + end + + end + result.assess_in_portfolio_only = %w(Yes y Y yes true TRUE 1).include? "#{row[:assess_in_portfolio_only]}".strip if result.valid? && (row[:group_set].blank? || result.group_set.present?) @@ -705,6 +736,24 @@ def task_assessment_resources(create = true) task_assessment_resources_with_abbreviation(abbreviation, create) end + def overseer_resource_files + return [] unless File.exist?(task_assessment_resources) + + files = [] + Zip::File.open(task_assessment_resources) do |zip_file| + zip_file.each do |entry| + next if entry.directory? + # skip macOS metadata files and hidden files + next if File.basename(entry.name).start_with?('._', '.') + + # remove top-level folder + parts = entry.name.split('/', 2) + files << "/#{parts.last}" unless parts.empty? + end + end + files + end + def task_assessment_script(create = true) task_assessment_script_with_abbreviation(abbreviation, create) end diff --git a/app/models/task_status.rb b/app/models/task_status.rb index 4e70314c6..b4dbac8cd 100644 --- a/app/models/task_status.rb +++ b/app/models/task_status.rb @@ -68,6 +68,10 @@ def self.assess_in_portfolio TaskStatus.find(13) end + def self.attention_required + TaskStatus.find(14) + end + class << self # Provide access to the count from the database via a new db_count method alias_method :db_count, :count @@ -80,7 +84,7 @@ class << self # Keep this hard coded! Saves cache load time. # Important: count must equal the largest id in the database def self.count - 13 + 14 end def self.status_for_name(name) @@ -111,6 +115,8 @@ def self.status_for_name(name) TaskStatus.time_exceeded when 'assess in portfolio', 'assess_in_portfolio', 'aip' TaskStatus.assess_in_portfolio + when 'attention required', 'attention_required', 'ar' + TaskStatus.attention_required else nil end @@ -135,6 +141,7 @@ def self.id_to_key(id) when 11 then :fail when 12 then :time_exceeded when 13 then :assess_in_portfolio + when 14 then :attention_required else :not_started end end @@ -153,6 +160,7 @@ def status_key return :feedback_exceeded if self == TaskStatus.feedback_exceeded return :time_exceeded if self == TaskStatus.time_exceeded return :assess_in_portfolio if self == TaskStatus.assess_in_portfolio + return :attention_required if self == TaskStatus.attention_required return :not_started end diff --git a/app/models/unit.rb b/app/models/unit.rb index 085e3968b..b01362f1f 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? @@ -378,9 +380,9 @@ def rollover(teaching_period, start_date, end_date, new_code) end end - # Duplicate task prerequisites task_definitions.each do |td| new_td = new_unit.task_definitions.find_by(abbreviation: td.abbreviation) + # Duplicate task prerequisites td.task_prerequisites.each do |prereq| new_prerequisite_td = new_unit.task_definitions.find_by(abbreviation: prereq.prerequisite.abbreviation) TaskPrerequisite.create!( @@ -389,6 +391,15 @@ def rollover(teaching_period, start_date, end_date, new_code) task_status_id: prereq.task_status_id ) end + + # Duplicate discussion prompts + td.discussion_prompts.each do |prompt| + DiscussionPrompt.create!({ + task_definition: new_td, + content: prompt.content, + priority: prompt.priority + }) + end end # Link outcomes @@ -2108,7 +2119,7 @@ def get_all_tasks_for(user, my_tutorials_only = false) # def tasks_awaiting_feedback(user) get_all_tasks_for(user) - .where('task_statuses.id IN (:ids)', ids: [TaskStatus.discuss, TaskStatus.redo, TaskStatus.demonstrate, TaskStatus.fix_and_resubmit]) + .where('task_statuses.id IN (:ids)', ids: [TaskStatus.discuss, TaskStatus.attention_required, TaskStatus.redo, TaskStatus.demonstrate, TaskStatus.fix_and_resubmit]) .order('task_definition_id') end diff --git a/app/models/unit_role.rb b/app/models/unit_role.rb index c6ee5de25..7a0050786 100644 --- a/app/models/unit_role.rb +++ b/app/models/unit_role.rb @@ -127,7 +127,7 @@ def populate_summary_stats(summary_stats, tutorial_stream, tutorial, row) data[:engagements] = all_engagements data[:total_staff_engagements] = all_engagements.count - data[:staff_engagements] = weekly_engagements.where(engagement: [TaskStatus.complete.name, TaskStatus.feedback_exceeded.name, TaskStatus.redo.name, TaskStatus.discuss.name, TaskStatus.demonstrate.name, TaskStatus.fail.name]) + data[:staff_engagements] = weekly_engagements.where(engagement: [TaskStatus.complete.name, TaskStatus.feedback_exceeded.name, TaskStatus.redo.name, TaskStatus.discuss.name, TaskStatus.attention_required.name, TaskStatus.demonstrate.name, TaskStatus.fail.name]) # Weekly task engagements for this tutorial data[:weekly_engagements_count] = weekly_engagements.count 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..0730b421b --- /dev/null +++ b/app/services/extension_service.rb @@ -0,0 +1,61 @@ +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 + + # ===== 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/sidekiq/accept_overseer_job.rb b/app/sidekiq/accept_overseer_job.rb index db0880f4b..3ab197c12 100644 --- a/app/sidekiq/accept_overseer_job.rb +++ b/app/sidekiq/accept_overseer_job.rb @@ -1,4 +1,5 @@ require 'yaml' +require 'open3' class AcceptOverseerJob include Sidekiq::Job @@ -9,7 +10,7 @@ class AcceptOverseerJob sidekiq_options lock: :until_executed, # TODO: should students be allowed to submit a new task submission when the previous overseer job has not started/completed? - lock_args_method: ->(args) { [args.first] }, + lock_args_method: ->(args) { [args.first, 'overseer-assessment'] }, on_conflict: :reject, retry: 1 @@ -19,90 +20,241 @@ def perform(task_id, _output_path, docker_image_name_tag, submission, assessment at(0) total(1) - work_dir = Rails.root.join("tmp", "overseer", task_id.to_s) - FileUtils.mkdir_p(work_dir) - task = Task.find(task_id) + task_definition = task.task_definition raise "PDF is still compiling" if task.processing_pdf? || !task.has_done_file? raise "Submission file not found: #{submission}" unless File.exist?(submission) - # Extract submission files, removing any parent folders - Zip::File.open(submission) do |zip_file| - zip_file.each do |entry| - next if entry.name_is_directory? + active_overseer_steps = task_definition.overseer_steps.select(&:enabled) - parts = entry.name.split('/')[1..] - next unless parts + raise "Task definition has no enabled overseer steps #{task.unit.detailed_name} #{task_definition.abbreviation}" if active_overseer_steps.empty? - dest_path = File.join(work_dir, *parts) - FileUtils.mkdir_p(File.dirname(dest_path)) - zip_file.extract(entry, dest_path) { true } + oa = OverseerAssessment.find(overseer_assessment_id) + oa.update!( + total_steps: active_overseer_steps.size + ) + + work_dir_name = "#{task.id}-#{overseer_assessment_id}" + + work_dir = Rails.root.join("tmp", "overseer", work_dir_name) + FileUtils.mkdir_p(work_dir) + + extract_student_submission_files(task, submission, work_dir) + extract_overseer_resource_files(assessment, work_dir) + + success_status = nil + failure_status = nil + + oa.add_assessment_comment("Tests in progress") + + steps_attempted = 0 + steps_passed = 0 + + assessment_pass = true + + active_overseer_steps.each do |step| + result = run_overseer_step( + step: step, + work_dir: work_dir, + work_dir_name: work_dir_name, + task_id: task_id, + timestamp: timestamp, + docker_image_name_tag: docker_image_name_tag, + overseer_assessment_id: overseer_assessment_id + ) + + steps_attempted += 1 + + if result.valid? && result.pass + steps_passed += 1 + if step.halt_on_success && step.status_on_success_id + success_status = TaskStatus.find(step.status_on_success_id) + break + end + elsif step.halt_on_failure + failure_status = TaskStatus.find(step.status_on_failure_id) if step.status_on_failure_id + assessment_pass = false + break end end - # Extract optional assessment resources - if File.exist?(assessment) - Zip::File.open(assessment) do |zip_file| - zip_file.each do |entry| - dest_path = File.join(work_dir, entry.name) - FileUtils.mkdir_p(File.dirname(dest_path)) - zip_file.extract(entry, dest_path) { true } # overwrite if exists - end + oa.update_assessment_comment("Tests complete: #{steps_passed} / #{active_overseer_steps.count}") + + if steps_attempted == steps_passed && assessment_pass + oa.update!(status: :passed) + unless success_status.nil? + # TODO: have an override status setting for the step? eg. if the task is overdue, let it remain overdue, otherwise use this task status + task.update!(task_status: success_status) + task.add_status_comment(task.project.tutor_for(task.task_definition), success_status) + + oa.update!(result_task_status: success_status.status_key.to_s) + end + else + oa.update!(status: :failed) + unless failure_status.nil? + # TODO: have an override status setting for the step? eg. if the task is overdue, let it remain overdue, otherwise use this task status + task.update!(task_status: failure_status) + task.add_status_comment(task.project.tutor_for(task.task_definition), failure_status) + oa.update!(result_task_status: failure_status.status_key.to_s) end + task.add_text_comment(task.project.tutor_for(task.task_definition), "**Automated comment**: Some tests did not pass for this submission. Please review the Overseer report, verify your output, and resubmit.") end - # Extract execution script - script_path = task.task_definition.task_assessment_script - raise "No execution script found" unless File.exist?(script_path) + FileUtils.rm_rf(work_dir) - script_contents = File.read(script_path) + logger.info "Completed overseer job" + rescue StandardError => e + logger.error e + raise e + end + + def run_overseer_step(step:, work_dir:, work_dir_name:, task_id:, timestamp:, docker_image_name_tag:, overseer_assessment_id:) + script_contents = step.run_command raise "Execution script is empty" if script_contents.blank? + # Create script run_sh_path = File.join(work_dir, 'run.sh') File.write(run_sh_path, script_contents) - system("chmod +x #{work_dir}/run.sh") + + # Ensure script is executable + system("chmod +x #{run_sh_path}") mount = Doubtfire::Application.config.overseer_workdir_volume_mount - volume_mount = if mount.nil? - # Fallback for development only — mounts the entire overseer container volume, - # allowing all task work directories to be accessible. This should never be - # used in production, as it breaks isolation between tasks. - "--volumes-from #{Doubtfire::Application.config.overseer_fallback_volume_container}" - else - "-v #{mount}/#{task_id}:/overseer/work-dir/#{task_id}" - end + volume_mount = + if mount.nil? + # Fallback for development only — mounts the entire overseer container volume, + # allowing all task work directories to be accessible. This should never be + # used in production, as it breaks isolation between tasks. + "--volumes-from #{Doubtfire::Application.config.overseer_fallback_volume_container}" + else + # Absolute path on the hosting server to the shared mount + "-v #{mount}/#{work_dir_name}:/overseer/work-dir/#{work_dir_name}" + end + + # Max runtime (seconds) before force-killing the step (exit status 124) + timeout = step.timeout + timeout = 30 if timeout.nil? || timeout.negative? container_name = "overseer-#{task_id}-#{timestamp}" command = %( - timeout 300 docker run --rm \ + timeout #{timeout} docker run --rm -i \ --cpus 1 \ --network none \ #{volume_mount} \ --name #{container_name} \ #{docker_image_name_tag} \ - bash -c "cd /overseer/work-dir/#{task_id} && ./run.sh" + bash -c "cd /overseer/work-dir/#{work_dir_name} && timeout #{timeout} ./run.sh" ) - system(command) + stdin_input_file = nil + expected_output_file = nil - yaml_path = File.join(work_dir, 'output.yaml') + # Retrieve names of input/output files + if step.step_type == 'output_diff' + stdin_input_file = step.stdin_input_file.present? ? File.join(work_dir, step.stdin_input_file) : nil + expected_output_file = step.expected_output_file.present? ? File.join(work_dir, step.expected_output_file) : nil + end - oa = OverseerAssessment.find(overseer_assessment_id) + output = "" + status = nil + stdin_contents = nil + + # Execute script and capture output + Open3.popen2e(command) do |stdin, stdout_err, wait_thr| + # If input file exists, pass it as standard input + if stdin_input_file && File.exist?(stdin_input_file) + File.open(stdin_input_file, 'rb') { |f| IO.copy_stream(f, stdin) } + stdin_contents = File.read(stdin_input_file) + stdin.close + end - oa.update_from_output(work_dir) - if File.exist?(yaml_path) - path = FileHelper.task_submission_identifier_path_with_timestamp(:done, task, timestamp) - FileUtils.cp(yaml_path, path) + stdout_err.each { |line| output << line } + status = wait_thr.value end - FileUtils.rm_rf(work_dir) + output = output.chomp + pass = status.exitstatus == 0 - logger.info "Completed overseer job" - rescue StandardError => e - logger.error e - raise e + expected_output_contents = nil + + # If step type is comparing output, retrieve expected output file contents + if step.step_type == 'output_diff' + expected_output_contents = + if expected_output_file && File.exist?(expected_output_file) + File.read(expected_output_file) + else + '' + end + matches_output = if step.partial_output_diff + output.include?(expected_output_contents) + else + output == expected_output_contents + end + + pass = false unless matches_output + end + + feedback_message = + if step.feedback_message.blank? + if step.step_type == 'output_diff' + "Your output did not match the expected result." + else + "This test did not complete successfully. Check the output for any errors." + end + else + step.feedback_message + end + + OverseerStepResult.create!( + overseer_assessment_id: overseer_assessment_id, + overseer_step: step, + exit_status: status.exitstatus, + pass: pass, + feedback_message: feedback_message, + stdout: output, + stdin: stdin_contents, + expected_output: expected_output_contents, + stdout_sha256: Digest::SHA256.hexdigest(output), + stdin_sha256: stdin_contents && Digest::SHA256.hexdigest(stdin_contents), + expected_output_sha256: expected_output_contents && Digest::SHA256.hexdigest(expected_output_contents) + ) + end + + def extract_student_submission_files(task, submission, work_dir) + # Extract submission files, removing any parent folders + Zip::File.open(submission) do |zip_file| + zip_file.each do |entry| + next if entry.name_is_directory? + + parts = entry.name.split('/')[1..] + next unless parts.length >= 1 + + file_name = parts.first + index = file_name.to_i + + file = task.upload_requirements[index] + final_name = file['name'] + + dest_path = File.join(work_dir, final_name) + FileUtils.mkdir_p(File.dirname(dest_path)) + zip_file.extract(entry, dest_path) { true } + end + end + end + + def extract_overseer_resource_files(assessment, work_dir) + # Extract optional assessment resources + if File.exist?(assessment) + Zip::File.open(assessment) do |zip_file| + zip_file.each do |entry| + dest_path = File.join(work_dir, entry.name) + FileUtils.mkdir_p(File.dirname(dest_path)) + zip_file.extract(entry, dest_path) { true } # overwrite if exists + end + end + end end end diff --git a/app/sidekiq/accept_submission_job.rb b/app/sidekiq/accept_submission_job.rb index b843f66cf..a8a44bf7f 100644 --- a/app/sidekiq/accept_submission_job.rb +++ b/app/sidekiq/accept_submission_job.rb @@ -2,7 +2,7 @@ class AcceptSubmissionJob include Sidekiq::Job include LogHelper - def perform(task_id, user_id, accepted_tii_eula) + def perform(task_id, user_id, accepted_tii_eula, test_submission) begin # Ensure cwd is valid... FileUtils.cd(Rails.root) @@ -49,6 +49,18 @@ def perform(task_id, user_id, accepted_tii_eula) if TurnItIn.enabled? task.send_documents_to_tii(user, accepted_tii_eula: accepted_tii_eula) end + + if task.overseer_enabled? || test_submission + overseer_assessment = OverseerAssessment.create_for(task, test_submission) + if overseer_assessment.present? + logger.info "Launching Overseer assessment for task_def_id: #{task.task_definition.id} task_id: #{task.id}" + + overseer_assessment.send_to_overseer(test_submission: test_submission) + + else + logger.info "Overseer assessment for task_def_id: #{task.task_definition.id} task_id: #{task.id} was not performed #{overseer_assessment.inspect}" + end + end rescue StandardError => e # to raise error message to avoid unnecessary retry logger.error e task.clear_in_process diff --git a/app/views/layouts/application.pdf.erbtex b/app/views/layouts/application.pdf.erbtex index 8e3db64fe..b05166180 100644 --- a/app/views/layouts/application.pdf.erbtex +++ b/app/views/layouts/application.pdf.erbtex @@ -42,6 +42,7 @@ \usepackage{lastpage} \usepackage{multirow} \usepackage[colorlinks]{hyperref} +\usepackage{longtable} \hypersetup{colorlinks, linkcolor=black, filecolor=black, 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/app/views/portfolio/portfolio_pdf.pdf.erb b/app/views/portfolio/portfolio_pdf.pdf.erb index 318dc625b..64704249c 100644 --- a/app/views/portfolio/portfolio_pdf.pdf.erb +++ b/app/views/portfolio/portfolio_pdf.pdf.erb @@ -257,16 +257,20 @@ end # if there are linked outcomes end # if outcomes exist if task.comments.count > 0 %> - \begin{tabular}{p{3cm}|p{3cm}|p{9cm}} + \begin{longtable}{p{3cm}|p{3cm}|p{9cm}} \textbf{Date} & \textbf{Author} & \textbf{Comment} \\ \hline <% task.comments.each do |comment| + full_text = comment.comment + chunks = full_text.scan(/.{1,2000}/m) # split into 2000-char chunks %> - <%= lesc comment.created_at.localtime.strftime("%Y/%m/%d %H:%M") %> & <%= lesc comment.user.name %> & <%= lesc comment.comment %> \\ + <% chunks.each_with_index do |chunk, i| %> + <%= lesc comment.created_at.localtime.strftime("%Y/%m/%d %H:%M") %> & <%= lesc comment.user.name %> & <%if i > 0 %>\textit{\textcolor{gray}{[continued...]}}<% end %> <%= lesc chunk %> <% if i < chunks.size - 1 %> \textit{\textcolor{gray}{[comment has been split due to length]}}<% end %> \\ + <% end %> <% end # comments loop %> - \end{tabular} + \end{longtable} <% end # if comments exist diff --git a/config/application.rb b/config/application.rb index 2b54558e5..19ea42155 100644 --- a/config/application.rb +++ b/config/application.rb @@ -27,6 +27,8 @@ class Application < Rails::Application # Load .env variables Dotenv::Rails.load + config.silence_healthcheck_path = "/health" + # ==> Authentication Method # Authentication method default is database, but possible settings # are: database, ldap, aaf, or saml. It can be overridden using the DF_AUTH_METHOD @@ -74,6 +76,7 @@ def self.fetch_boolean_env(name) # variable. config.jplag_report_dir = ENV['DF_JPLAG_REPORT_DIR'] || Rails.root.join('jplag/results').to_s config.jplag_min_tokens = ENV.fetch('DF_JPLAG_MIN_TOKENS', -1) + config.jplag_skip_cluster_check = ENV['DF_JPLAG_SKIP_CLUSTER_CHECK'].present? && (ENV['DF_JPLAG_SKIP_CLUSTER_CHECK'].to_s.downcase == "true" || ENV['DF_JPLAG_SKIP_CLUSTER_CHECK'].to_i == 1) # ==> File size limits # Sets the global file size limit per upload requirement 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/config/routes.rb b/config/routes.rb index 60479c128..ea52a7900 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -9,4 +9,6 @@ mount ApiRoot => '/' mount GrapeSwaggerRails::Engine => '/api/docs' mount Sidekiq::Web => "/sidekiq" # mount Sidekiq::Web in your Rails app + + get "health" => "rails/health#show", as: :rails_health_check end 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/migrate/20251110000046_create_discussion_prompts.rb b/db/migrate/20251110000046_create_discussion_prompts.rb new file mode 100644 index 000000000..789c4101a --- /dev/null +++ b/db/migrate/20251110000046_create_discussion_prompts.rb @@ -0,0 +1,10 @@ +class CreateDiscussionPrompts < ActiveRecord::Migration[8.0] + def change + create_table :discussion_prompts do |t| + t.references :task_definition, null: false + t.text :content, null: false, limit: 4096 + t.integer :priority, default: 0 + t.timestamps + end + end +end diff --git a/db/migrate/20251124015104_add_project_task_deadlines.rb b/db/migrate/20251124015104_add_project_task_deadlines.rb new file mode 100644 index 000000000..0caf2a82a --- /dev/null +++ b/db/migrate/20251124015104_add_project_task_deadlines.rb @@ -0,0 +1,6 @@ +class AddProjectTaskDeadlines < ActiveRecord::Migration[8.0] + def change + add_column :tasks, :target_start_date, :datetime + add_column :tasks, :target_due_date, :datetime + end +end diff --git a/db/migrate/20251212010033_add_attention_required_status.rb b/db/migrate/20251212010033_add_attention_required_status.rb new file mode 100644 index 000000000..ca88e635c --- /dev/null +++ b/db/migrate/20251212010033_add_attention_required_status.rb @@ -0,0 +1,7 @@ +class AddAttentionRequiredStatus < ActiveRecord::Migration[8.0] + def change + if TaskStatus.where(name: 'Attention Required').count < 1 + TaskStatus.create name: "Attention Required", description: "This task needs to be discussed with your tutor so that you can get back on track." + end + end +end diff --git a/db/migrate/20251218031455_create_overseer_steps.rb b/db/migrate/20251218031455_create_overseer_steps.rb new file mode 100644 index 000000000..eab2bafe4 --- /dev/null +++ b/db/migrate/20251218031455_create_overseer_steps.rb @@ -0,0 +1,70 @@ +class CreateOverseerSteps < ActiveRecord::Migration[8.0] + def change + create_table :overseer_steps do |t| + t.references :task_definition, null: false + + # Staff only + t.string :name, null: false + t.text :description + + # Shown to the student + t.string :display_name, null: false + t.string :display_description + + t.text :run_command + + t.integer :timeout, default: 30, null: false + t.integer :sort_order, default: 0, null: false + + t.string :step_type, null: false # "status_check", "output_diff", etc. + t.boolean :partial_output_diff + + t.string :stdin_input_file # Name of file (or path) in assessment resources + t.string :expected_output_file # Name of file in (or path) assessment resources + + t.text :feedback_message + + t.references :status_on_success + t.references :status_on_failure + + t.boolean :halt_on_success + t.boolean :halt_on_failure + + t.boolean :show_expected_output + t.boolean :show_stdin + t.boolean :show_stdout + + t.boolean :enabled, default: true + + t.timestamps + end + + create_table :overseer_step_results do |t| + t.references :overseer_assessment, null: false + t.references :overseer_step, null: false + + t.integer :exit_status, null: false, default: -1 + t.boolean :pass, null: false, default: false + + t.text :feedback_message + + # The output from the overseer script and student's submission + t.text :stdout + + # The original input/output files, in case they have since been changed + t.text :stdin + t.text :expected_output + + # We may want to discard the original_stdin, expected_output, and stdout when archiving a unit. + # Storing hashes will allow us to confirm if the original outputs matched + t.string :stdout_sha256 + t.string :stdin_sha256 + t.string :expected_output_sha256 + + t.timestamps + end + + # Track the number of available steps at the time of assessment + add_column :overseer_assessments, :total_steps, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index 0e65657d5..56f400927 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_18_031455) 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,140 @@ 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.integer "total_steps" 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" + create_table "overseer_step_results", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.bigint "overseer_assessment_id", null: false + t.bigint "overseer_step_id", null: false + t.integer "exit_status", default: -1, null: false + t.boolean "pass", default: false, null: false + t.text "feedback_message" + t.text "stdout" + t.text "stdin" + t.text "expected_output" + t.string "stdout_sha256" + t.string "stdin_sha256" + t.string "expected_output_sha256" 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.index ["overseer_assessment_id"], name: "index_overseer_step_results_on_overseer_assessment_id" + t.index ["overseer_step_id"], name: "index_overseer_step_results_on_overseer_step_id" + end + + create_table "overseer_steps", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.bigint "task_definition_id", null: false + t.string "name", null: false + t.text "description" + t.string "display_name", null: false + t.string "display_description" + t.text "run_command" + t.integer "timeout", default: 30, null: false + t.integer "sort_order", default: 0, null: false + t.string "step_type", null: false + t.boolean "partial_output_diff" + t.string "stdin_input_file" + t.string "expected_output_file" + t.text "feedback_message" + t.bigint "status_on_success_id" + t.bigint "status_on_failure_id" + t.boolean "halt_on_success" + t.boolean "halt_on_failure" + t.boolean "show_expected_output" + t.boolean "show_stdin" + t.boolean "show_stdout" + t.boolean "enabled", default: true + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["status_on_failure_id"], name: "index_overseer_steps_on_status_on_failure_id" + t.index ["status_on_success_id"], name: "index_overseer_steps_on_status_on_success_id" + t.index ["task_definition_id"], name: "index_overseer_steps_on_task_definition_id" + end + + create_table "projects", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.bigint "assessor_id" + t.bigint "campus_id" t.boolean "compile_portfolio", default: false - t.date "portfolio_production_date" - t.bigint "user_id" + t.datetime "created_at", null: false + t.boolean "enrolled", default: true 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 +318,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 +340,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 +354,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 +386,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 +427,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 +458,61 @@ 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.datetime "target_start_date" + t.datetime "target_due_date" 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 +521,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 +565,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 +596,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 +619,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 +639,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 +653,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 +687,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 +742,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/lib/tasks/init.rake b/lib/tasks/init.rake index 6a4c4bbc9..2023e509a 100644 --- a/lib/tasks/init.rake +++ b/lib/tasks/init.rake @@ -41,7 +41,8 @@ namespace :db do Demonstrate: "Your work looks good, demonstrate it to your tutor to complete.", Fail: "You did not successfully demonstrate the required learning in this task.", "Time Exceeded": "You did not submit or complete the task before the appropriate deadline.", - "Assess in Portfolio": "This task will not be signed off as complete by your tutor, and will be marked directly in your portfolio." + "Assess in Portfolio": "This task will not be signed off as complete by your tutor, and will be marked directly in your portfolio.", + "Attention Required": "This task needs to be discussed with your tutor so that you can get back on track." } statuses.each do |name, desc| print "." diff --git a/lib/tasks/maintenance.rake b/lib/tasks/maintenance.rake index 5f68a071c..f6ba8ebfc 100644 --- a/lib/tasks/maintenance.rake +++ b/lib/tasks/maintenance.rake @@ -24,6 +24,12 @@ namespace :maintenance do end end + # Destroy old marking sessions that have less than 1 minute duration + MarkingSession + .where("end_time IS NOT NULL AND end_time < ?", 24.hours.ago) + .where("TIMESTAMPDIFF(SECOND, start_time, end_time) <= ?", 60) + .find_each(&:destroy!) + AuthToken.destroy_old_tokens end diff --git a/test/api/discussion_prompts_api_test.rb b/test/api/discussion_prompts_api_test.rb new file mode 100644 index 000000000..c7c1db70d --- /dev/null +++ b/test/api/discussion_prompts_api_test.rb @@ -0,0 +1,179 @@ +require 'test_helper' +require 'date' +require './lib/helpers/database_populator' + +class DiscussionPromptsApiTest < ActiveSupport::TestCase + include Rack::Test::Methods + include TestHelpers::AuthHelper + include TestHelpers::JsonHelper + include TestHelpers::TestFileHelper + + def app + Rails.application + end + + def test_dicussion_prompts_filters + # Create discussion prompts for task definition + unit = FactoryBot.create(:unit) + user = FactoryBot.create(:user, :convenor) + unit.employ_staff(user, Role.convenor) + + td1 = FactoryBot.create(:task_definition, unit: unit) + td2 = FactoryBot.create(:task_definition, unit: unit) + + assert_not_nil td1 + assert_not_nil td2 + + student1 = FactoryBot.create(:user, :student) + student2 = FactoryBot.create(:user, :student) + + project1 = unit.enrol_student(student1, nil) + project2 = unit.enrol_student(student2, nil) + + # Create global prompts (for task definitions) + DiscussionPrompt.create!({ + task_definition: td1, + content: 'Ask the student about pointers and references...', + priority: 1 + }) + + DiscussionPrompt.create!({ + task_definition: td2, + content: 'Ask the student about passing values by reference...', + priority: 2 + }) + + DiscussionPrompt.create!({ + task_definition: td1, + content: 'Potential use of GenAI, ask the student to explain their code...', + priority: 3 + }) + + DiscussionPrompt.create!({ + task_definition: td2, + content: 'Ask student to explain why they used std:cin over read_line()...', + priority: 4 + }) + + DiscussionPrompt.create!({ + task_definition: td2, + content: 'Ask student to explain why they used std:cin over read_line()... 1', + priority: 4 + }) + + add_auth_header_for(user: user) + + get "/api/task_definitions/#{td1.id}/discussion_prompts" + assert_equal 200, last_response.status, last_response_body.inspect + assert_equal 2, last_response_body.count + + get "/api/task_definitions/#{td2.id}/discussion_prompts" + assert_equal 200, last_response.status, last_response_body.inspect + assert_equal 3, last_response_body.count + + project1_task1 = project1.task_for_task_definition(td1) + project1_task2 = project1.task_for_task_definition(td2) + + project2_task1 = project2.task_for_task_definition(td1) + project2_task2 = project2.task_for_task_definition(td2) + + project1_task1.update!(task_status: TaskStatus.discuss) + project1_task2.update!(task_status: TaskStatus.complete) + + project2_task1.update!(task_status: TaskStatus.ready_for_feedback) + project2_task2.update!(task_status: TaskStatus.complete) + + get "/api/projects/#{project1.id}/discussion_prompts" + assert_equal 200, last_response.status, last_response_body.inspect + assert_equal 2, last_response_body.count + + project1_task2.update!(task_status: TaskStatus.discuss) + + get "/api/projects/#{project1.id}/discussion_prompts" + assert_equal 200, last_response.status, last_response_body.inspect + assert_equal 5, last_response_body.count + + get "/api/projects/#{project2.id}/discussion_prompts" + assert_equal 200, last_response.status, last_response_body.inspect + assert_equal 0, last_response_body.count + end + + def test_student_dicussion_prompts_permissions + unit = FactoryBot.create(:unit) + convenor = FactoryBot.create(:user, :convenor) + tutor = FactoryBot.create(:user, :tutor) + + unit.employ_staff(convenor, Role.convenor) + unit.employ_staff(tutor, Role.tutor) + + td = FactoryBot.create(:task_definition, unit: unit) + + student = FactoryBot.create(:user, :student) + project = unit.enrol_student(student, nil) + + users_can = [convenor, tutor] + + users_cant = [student] + + users_can.each do |user| + add_auth_header_for(user: user) + + get "/api/task_definitions/#{td.id}/discussion_prompts" + assert_equal 200, last_response.status, last_response_body + + post "/api/task_definitions/#{td.id}/discussion_prompts", { + content: 'test content 123', + priority: 1 + } + assert_equal 201, last_response.status, last_response_body + + last_prompt = DiscussionPrompt.last + + assert_not_nil last_prompt + assert_equal 'test content 123', last_prompt.content + assert_equal 1, last_prompt.priority + + put "/api/task_definitions/#{td.id}/discussion_prompts/#{last_prompt.id}", { + content: 'test content 456', + priority: 2 + } + assert_equal 200, last_response.status, last_response_body + last_prompt.reload + assert_equal 'test content 456', last_prompt.content + assert_equal 2, last_prompt.priority + + delete "/api/task_definitions/#{td.id}/discussion_prompts/#{last_prompt.id}" + assert_equal 200, last_response.status, last_response_body + assert_nil DiscussionPrompt.find_by(id: last_prompt.id) + + get "/api/projects/#{project.id}/discussion_prompts" + assert_equal 200, last_response.status, last_response_body + end + + users_cant.each do |user| + add_auth_header_for(user: user) + + get "/api/task_definitions/#{td.id}/discussion_prompts" + assert_equal 403, last_response.status, last_response_body + + post "/api/task_definitions/#{td.id}/discussion_prompts", { + content: 'test', + priority: 0 + } + assert_equal 403, last_response.status, last_response_body + + put "/api/task_definitions/#{td.id}/discussion_prompts/1", { + content: 'test', + priority: 0 + } + assert_equal 403, last_response.status, last_response_body + + delete "/api/task_definitions/#{td.id}/discussion_prompts/1" + assert_equal 403, last_response.status, last_response_body + + get "/api/projects/#{project.id}/discussion_prompts" + assert_equal 403, last_response.status, last_response_body + end + end + +end diff --git a/test/api/staff_grant_extension_test.rb b/test/api/staff_grant_extension_test.rb new file mode 100644 index 000000000..a90d74dcd --- /dev/null +++ b/test/api/staff_grant_extension_test.rb @@ -0,0 +1,256 @@ +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.name}/, 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..7848aaec8 100644 --- a/test/api/tasks_api_test.rb +++ b/test/api/tasks_api_test.rb @@ -1012,4 +1012,66 @@ def test_resubmission_doesnt_change_submission_date assert task1.submission_date > task2.submission_date end end + + def test_task_target_date_permissions + unit = FactoryBot.create(:unit, task_count: 2, student_count: 1) + # tutor = FactoryBot.create(:user, :tutor) + # convenor = FactoryBot.create(:user, :convenor) + + # convenor_role = unit.employ_staff(convenor, Role.convenor) + # tutor_role = unit.employ_staff(tutor, Role.tutor) + + td = unit.task_definitions.first + + original_start_date = Time.zone.today + original_end_date = Time.zone.today + 1.day + td.update!(target_date: original_end_date, start_date: original_start_date) + assert_not td.nil? + + student1 = FactoryBot.create(:user, :student) + + project1 = unit.enrol_student(student1, nil) + task = project1.task_for_task_definition(td) + + assert_equal original_end_date, task.due_date + + unit.update!(allow_flexible_dates: false) + + new_start_date = Time.zone.today + 2.days + new_end_date = Time.zone.today + 4.days + + add_auth_header_for(user: student1) + + put "/api/projects/#{project1.id}/task_def_id/#{td.id}/target_dates", { + target_start_date: new_start_date, + target_due_date: new_end_date + } + + assert_equal 403, last_response.status + assert_equal original_end_date, task.due_date + + unit.update!(allow_flexible_dates: true) + + put "/api/projects/#{project1.id}/task_def_id/#{td.id}/target_dates" + assert_equal 400, last_response.status # target_start_date and target_due_date are required + + put "/api/projects/#{project1.id}/task_def_id/#{td.id}/target_dates", { + target_start_date: new_start_date, + target_due_date: new_end_date + } + assert_equal 200, last_response.status + + task.reload + assert_equal new_start_date, task.target_start_date + assert_equal new_end_date, task.target_due_date + + put "/api/projects/#{project1.id}/reset_target_dates" + assert_equal 200, last_response.status + task.reload + assert_nil task.target_start_date + assert_nil task.target_due_date + + unit.update!(allow_flexible_dates: false) + end + end diff --git a/test/factories/overseer_steps.rb b/test/factories/overseer_steps.rb new file mode 100644 index 000000000..284b2e6b1 --- /dev/null +++ b/test/factories/overseer_steps.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + factory :overseer_step do + + end +end diff --git a/test/mailers/notifications_mailer_test.rb b/test/mailers/notifications_mailer_test.rb new file mode 100644 index 000000000..393b38029 --- /dev/null +++ b/test/mailers/notifications_mailer_test.rb @@ -0,0 +1,226 @@ +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.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] = { data: "test data" } + + # 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 diff --git a/test/models/overseer_step_test.rb b/test/models/overseer_step_test.rb new file mode 100644 index 000000000..6cce7d6fa --- /dev/null +++ b/test/models/overseer_step_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +describe OverseerStep do + # it "does a thing" do + # value(1+1).must_equal 2 + # end +end diff --git a/test/models/task_definition_test.rb b/test/models/task_definition_test.rb index 2d47e1f45..77f9d9ed5 100644 --- a/test/models/task_definition_test.rb +++ b/test/models/task_definition_test.rb @@ -145,7 +145,7 @@ def test_export_task_definitions_csv task_defs_csv = CSV.parse unit.task_definitions_csv, headers: true task_defs_csv.each do |task_def_csv| task_def = unit.task_definitions.find_by(abbreviation: task_def_csv['abbreviation']) - keys_to_ignore = %w[tutorial_stream start_week start_day target_week target_day due_week due_day upload_requirements task_prerequisites] + keys_to_ignore = %w[tutorial_stream start_week start_day target_week target_day due_week due_day upload_requirements task_prerequisites discussion_prompts] task_def_csv.each do |key, value| unless keys_to_ignore.include?(key) assert_equal(task_def[key].to_s, value) diff --git a/test/models/task_status_test.rb b/test/models/task_status_test.rb index eab479a33..e5081c59d 100644 --- a/test/models/task_status_test.rb +++ b/test/models/task_status_test.rb @@ -508,11 +508,11 @@ def test_status_for_name end def test_staff_assigned_statuses - assert_equal TaskStatus.staff_assigned_statuses.count, 9 # number of staff tasks + assert_equal TaskStatus.staff_assigned_statuses.count, 10 # number of staff tasks end def test_id_to_key_not_started - assert_equal TaskStatus.id_to_key(14), :not_started + assert_equal TaskStatus.id_to_key(15), :not_started end end diff --git a/test/models/unit_model_test.rb b/test/models/unit_model_test.rb index 81ea40fec..6a54c9b28 100644 --- a/test/models/unit_model_test.rb +++ b/test/models/unit_model_test.rb @@ -301,6 +301,58 @@ def test_rollover_assess_in_portfolio unit3.destroy! end + def test_rollover_of_discussion_prompts + unit = FactoryBot.create(:unit, with_students: false, task_count: 4) + td1 = unit.task_definitions.first + td2 = unit.task_definitions.second + + DiscussionPrompt.create!({ + task_definition: td1, + content: 'Discuss pointers and references', + priority: 1 + }) + + DiscussionPrompt.create!({ + task_definition: td1, + content: 'Discuss object oriented programming', + priority: 2 + }) + + DiscussionPrompt.create!({ + task_definition: td2, + content: 'Discuss use of AI', + priority: 3 + }) + + unit2 = unit.rollover(TeachingPeriod.find(2), nil, nil, nil) + + new_td1 = unit2.task_definitions.first + new_td2 = unit2.task_definitions.second + + assert_equal 2, new_td1.discussion_prompts.count + assert_equal 1, new_td2.discussion_prompts.count + + new_prompt1 = new_td1.discussion_prompts.first + new_prompt2 = new_td1.discussion_prompts.second + new_prompt3 = new_td2.discussion_prompts.first + + assert_not_nil new_prompt1, "Discussion prompt should be duplicated in rollover" + assert_not_nil new_prompt2, "Discussion prompt should be duplicated in rollover" + assert_not_nil new_prompt3, "Discussion prompt should be duplicated in rollover" + + assert_equal new_prompt1.task_definition.id, new_td1.id + assert_equal new_prompt1.content, 'Discuss pointers and references' + assert_equal new_prompt1.priority, 1 + + assert_equal new_prompt2.task_definition.id, new_td1.id + assert_equal new_prompt2.content, 'Discuss object oriented programming' + assert_equal new_prompt2.priority, 2 + + assert_equal new_prompt3.task_definition.id, new_td2.id + assert_equal new_prompt3.content, 'Discuss use of AI' + assert_equal new_prompt3.priority, 3 + end + def test_rollover_of_task_prerequisites unit = FactoryBot.create(:unit, with_students: false, task_count: 4) td1 = unit.task_definitions.first diff --git a/test_files/COS10001-ImportTasksWithTutorialStream.csv b/test_files/COS10001-ImportTasksWithTutorialStream.csv index 7b6ea2e63..bc92146d6 100644 --- a/test_files/COS10001-ImportTasksWithTutorialStream.csv +++ b/test_files/COS10001-ImportTasksWithTutorialStream.csv @@ -1,37 +1,37 @@ -name,abbreviation,description,weighting,target_grade,restrict_status_updates,upload_requirements,start_week,start_day,target_week,target_day,due_week,due_day,max_quality_pts,is_graded,plagiarism_warn_pct,plagiarism_checks,group_set,tutorial_stream,scorm_enabled,scorm_allow_review,scorm_bypass_test,scorm_time_delay_enabled,scorm_attempt_limit,assess_in_portfolio_only,task_prerequisites -Pass Task 1.1 - Hello World,1.1P,"As a first step, create the classic 'Hello World' program. This will help ensure that you have all of the software installed correctly, and are ready to move on with creating other,,, programs.",1,0,FALSE,"[{""key"":""file0"",""name"":""HelloWorld.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 1.2 - Picture Drawing,1.2P,Create a program that calls procedures to draw a picture to a window (something other than a house which we use as the example).,2,0,FALSE,"[{""key"":""file0"",""name"":""PictureDrawing.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 1.3 - Creating a Procedure,1.3P,"Now that you have created a program that uses procedures, you can learn how to create your own procedures. Creating procedures will allow you to group your program's actions into procedures that perform meaningful tasks.",2,0,FALSE,"[{""key"":""file0"",""name"":""PictureDrawing.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Credit Task 1.4 - Concept Map,1.4C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",1,Tue,2,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 2.1 - Hand Execute Assignment,2.1P,"Using the assignment statement, you can assign a value to a variable. In this task you will demonstrate how this action works within the computer.",2,0,FALSE,"[{""key"":""file0"",""name"":""Program Execution 1"",""type"":""image""},{""key"":""file1"",""name"":""Program Execution 2"",""type"":""image""},{""key"":""file2"",""name"":""Program Execution 3"",""type"":""image""},{""key"":""file3"",""name"":""Program Execution 4"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 2.2 - Hello User,2.2P,Now that we have variables we can create a program that reads in the users name from the Terminal and echoes back a welcome message.,4,0,FALSE,"[{""key"":""file0"",""name"":""HelloUser.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 2.3 - My Drawing Procedure,2.3P,Procedures are a great way of encapsulating the instructions needed to perform a task. In most cases the task will need some input data for it to work with. Use parameters to provide data to your procedures.,2,0,FALSE,"[{""key"":""file0"",""name"":""Shape Drawing Code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 2.4 - My Functions,2.4P,Using functions you can now create artefacts to encapsulate the steps needed to calculate a value.,4,0,FALSE,"[{""key"":""file0"",""name"":""My Function Code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Credit Task 2.5 - Concept Maps,2.5C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",2,Tue,3,Tue,,,5,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 3.1 - Hand Execution of Control Flow,3.1P,In this task you will use the hand execution process to demonstrate how the control flow constructs operate within the computer.,2,0,FALSE,"[{""key"":""file0"",""name"":""Button Code"",""type"":""code""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 3.2 - Name Tester,3.2P,Control flow enables you to easily add conditions and loops to your programs. In this task you will create a small program that uses conditions and loops to output custom messages to users.,4,0,FALSE,"[{""key"":""file0"",""name"":""Name Tester code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 3.3 - Circle Moving,3.3P,In this task you will create a small program that allows the user to move a circle around on the screen.,4,0,FALSE,"[{""key"":""file0"",""name"":""Circle Mover code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Credit Task 3.4 - User Input Functions,3.4C,So far we have provided you with a unit to read and check values entered by the user: the Terminal User Input unit. In this task you will extend this library so that it has a number of additional functions.,4,1,FALSE,"[{""key"":""file0"",""name"":""User Input unit code"",""type"":""code""},{""key"":""file1"",""name"":""Program code"",""type"":""code""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Credit Task 3.5 - Concept Map,3.5C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",3,Tue,4,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Distinction Task 3.6 - Mandelbrot,3.6D,The Mandelbrot provides an interesting challenge in order to determine how to zoom in to and out of the section of the Mandelbrot being shown to the user.,4,2,FALSE,"[{""key"":""file0"",""name"":""Mandelbrot code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 4.1 - Using Records and Enumerations,4.1P,Effectively organising your data makes programs much easier to develop. By using records and enumerations you can start to model the entities associated with your programs.,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",4,Tue,5,Tue,7,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Credit Task 4.2 - Fruit Punch,4.2C,Create a program using the concepts covered so far.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",4,Tue,5,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Credit Task 4.3 - Concept Map,4.3C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",4,Tue,5,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Test 1,T1,Test 1 covers weeks 1 to 3,1,0,TRUE,[],5,Fri,5,Fri,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 5.1 - Hand Execution of Arrays,5.1P,Demonstrate how arrays work within the computer.,2,0,FALSE,"[{""key"":""file0"",""name"":""Execution of Program 1"",""type"":""image""},{""key"":""file1"",""name"":""Execution of Program 2"",""type"":""image""}]",5,Tue,6,Tue,10,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 5.2 - Arrays of Records,5.2P,Add an array of records to your program that uses records.,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,10,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Credit Task 5.3 - Food Hunter,5.3C,Extend a small game to make use of arrays.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Credit Task 5.4 - Concept Map,5.4C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",5,Tue,6,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Distinction Task 5.5 - Sort Visualiser,5.5D,Create a program to demonstrate sorting working within the computer.,4,2,FALSE,"[{""key"":""file0"",""name"":""Sort Visualiser"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,"[{""abbreviation"":""1.4C"",""task_status_id"":2},{""abbreviation"":""3.3P"",""task_status_id"":9}]" -Pass Task 6.1 - Structure Charts,6.1P,Illustrate the structure of your program using a structure chart.,2,0,FALSE,"[{""key"":""file0"",""name"":""Program structrue chart"",""type"":""image""}]",6,Tue,7,Tue,10,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 7.1 - Programming Principles,7.1P,"Describe the principles of structured, procedural, programming.",4,0,FALSE,"[{""key"":""file0"",""name"":""Program Principles Description"",""type"":""document""}]",7,Tue,8,Tue,10,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Distinction Task 7.2 - Game of Life,7.2D,Create the Game of Life,4,2,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",7,Tue,8,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 8.1 - Language Reference Sheet,8.1P,Create a reference sheet for C or C#,4,0,FALSE,"[{""key"":""file0"",""name"":""Reference Sheet"",""type"":""document""}]",8,Tue,9,Tue,10,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 8.2 - Circle Moving 2,8.2P,Recreate your circle moving program using C,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",8,Tue,9,Tue,10,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Test 2,T2,Covers all core concepts.,1,0,TRUE,[],9,Fri,9,Fri,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 9.1 - Reading Another Language,9.1P,Demonstrate how programs written in C work within the computer,2,0,FALSE,"[{""key"":""file0"",""name"":""Execution of Program 1"",""type"":""image""},{""key"":""file1"",""name"":""Execution of Program 2"",""type"":""image""}]",9,Tue,10,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Credit Task 9.2 - Another Language,9.2C,Create a program with C using the concepts covered.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",9,Tue,10,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -High Distinction Task 10.1 - Custom Program,10.1H,Extend your custom program to meet the High Distinction criteria.,4,3,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Design overview"",""type"":""document""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",10,Tue,13,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -High Distinction Task 10.2 - Research Report,10.2H,Start working on a research project,8,3,FALSE,"[{""key"":""file0"",""name"":""Research Report Document"",""type"":""document""}]",10,Tue,13,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Pass Task 11.1 - Learning Summary Report,11.1P,Summarise your learning from the unit.,4,0,FALSE,"[{""key"":""file0"",""name"":""Learning Summary Report"",""type"":""document""}]",11,Tue,12,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[] -Distinction Task 6.2 - Custom Program,6.2D,Start working on your custom program!,16,2,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Design overview"",""type"":""document""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",6,Tue,13,Tue,,,5,TRUE,90,,,import-tasks,,,,FALSE,[] +name,abbreviation,description,weighting,target_grade,restrict_status_updates,upload_requirements,start_week,start_day,target_week,target_day,due_week,due_day,max_quality_pts,is_graded,plagiarism_warn_pct,plagiarism_checks,group_set,tutorial_stream,scorm_enabled,scorm_allow_review,scorm_bypass_test,scorm_time_delay_enabled,scorm_attempt_limit,assess_in_portfolio_only,task_prerequisites,discussion_prompts +Pass Task 1.1 - Hello World,1.1P,"As a first step, create the classic 'Hello World' program. This will help ensure that you have all of the software installed correctly, and are ready to move on with creating other,,, programs.",1,0,FALSE,"[{""key"":""file0"",""name"":""HelloWorld.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 1.2 - Picture Drawing,1.2P,Create a program that calls procedures to draw a picture to a window (something other than a house which we use as the example).,2,0,FALSE,"[{""key"":""file0"",""name"":""PictureDrawing.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 1.3 - Creating a Procedure,1.3P,"Now that you have created a program that uses procedures, you can learn how to create your own procedures. Creating procedures will allow you to group your program's actions into procedures that perform meaningful tasks.",2,0,FALSE,"[{""key"":""file0"",""name"":""PictureDrawing.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Credit Task 1.4 - Concept Map,1.4C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",1,Tue,2,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 2.1 - Hand Execute Assignment,2.1P,"Using the assignment statement, you can assign a value to a variable. In this task you will demonstrate how this action works within the computer.",2,0,FALSE,"[{""key"":""file0"",""name"":""Program Execution 1"",""type"":""image""},{""key"":""file1"",""name"":""Program Execution 2"",""type"":""image""},{""key"":""file2"",""name"":""Program Execution 3"",""type"":""image""},{""key"":""file3"",""name"":""Program Execution 4"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 2.2 - Hello User,2.2P,Now that we have variables we can create a program that reads in the users name from the Terminal and echoes back a welcome message.,4,0,FALSE,"[{""key"":""file0"",""name"":""HelloUser.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 2.3 - My Drawing Procedure,2.3P,Procedures are a great way of encapsulating the instructions needed to perform a task. In most cases the task will need some input data for it to work with. Use parameters to provide data to your procedures.,2,0,FALSE,"[{""key"":""file0"",""name"":""Shape Drawing Code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 2.4 - My Functions,2.4P,Using functions you can now create artefacts to encapsulate the steps needed to calculate a value.,4,0,FALSE,"[{""key"":""file0"",""name"":""My Function Code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Credit Task 2.5 - Concept Maps,2.5C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",2,Tue,3,Tue,,,5,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 3.1 - Hand Execution of Control Flow,3.1P,In this task you will use the hand execution process to demonstrate how the control flow constructs operate within the computer.,2,0,FALSE,"[{""key"":""file0"",""name"":""Button Code"",""type"":""code""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 3.2 - Name Tester,3.2P,Control flow enables you to easily add conditions and loops to your programs. In this task you will create a small program that uses conditions and loops to output custom messages to users.,4,0,FALSE,"[{""key"":""file0"",""name"":""Name Tester code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 3.3 - Circle Moving,3.3P,In this task you will create a small program that allows the user to move a circle around on the screen.,4,0,FALSE,"[{""key"":""file0"",""name"":""Circle Mover code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Credit Task 3.4 - User Input Functions,3.4C,So far we have provided you with a unit to read and check values entered by the user: the Terminal User Input unit. In this task you will extend this library so that it has a number of additional functions.,4,1,FALSE,"[{""key"":""file0"",""name"":""User Input unit code"",""type"":""code""},{""key"":""file1"",""name"":""Program code"",""type"":""code""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Credit Task 3.5 - Concept Map,3.5C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",3,Tue,4,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Distinction Task 3.6 - Mandelbrot,3.6D,The Mandelbrot provides an interesting challenge in order to determine how to zoom in to and out of the section of the Mandelbrot being shown to the user.,4,2,FALSE,"[{""key"":""file0"",""name"":""Mandelbrot code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 4.1 - Using Records and Enumerations,4.1P,Effectively organising your data makes programs much easier to develop. By using records and enumerations you can start to model the entities associated with your programs.,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",4,Tue,5,Tue,7,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Credit Task 4.2 - Fruit Punch,4.2C,Create a program using the concepts covered so far.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",4,Tue,5,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Credit Task 4.3 - Concept Map,4.3C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",4,Tue,5,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Test 1,T1,Test 1 covers weeks 1 to 3,1,0,TRUE,[],5,Fri,5,Fri,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 5.1 - Hand Execution of Arrays,5.1P,Demonstrate how arrays work within the computer.,2,0,FALSE,"[{""key"":""file0"",""name"":""Execution of Program 1"",""type"":""image""},{""key"":""file1"",""name"":""Execution of Program 2"",""type"":""image""}]",5,Tue,6,Tue,10,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 5.2 - Arrays of Records,5.2P,Add an array of records to your program that uses records.,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,10,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Credit Task 5.3 - Food Hunter,5.3C,Extend a small game to make use of arrays.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Credit Task 5.4 - Concept Map,5.4C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",5,Tue,6,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Distinction Task 5.5 - Sort Visualiser,5.5D,Create a program to demonstrate sorting working within the computer.,4,2,FALSE,"[{""key"":""file0"",""name"":""Sort Visualiser"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,"[{""abbreviation"":""1.4C"",""task_status_id"":2},{""abbreviation"":""3.3P"",""task_status_id"":9}]",[] +Pass Task 6.1 - Structure Charts,6.1P,Illustrate the structure of your program using a structure chart.,2,0,FALSE,"[{""key"":""file0"",""name"":""Program structrue chart"",""type"":""image""}]",6,Tue,7,Tue,10,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 7.1 - Programming Principles,7.1P,"Describe the principles of structured, procedural, programming.",4,0,FALSE,"[{""key"":""file0"",""name"":""Program Principles Description"",""type"":""document""}]",7,Tue,8,Tue,10,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Distinction Task 7.2 - Game of Life,7.2D,Create the Game of Life,4,2,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",7,Tue,8,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 8.1 - Language Reference Sheet,8.1P,Create a reference sheet for C or C#,4,0,FALSE,"[{""key"":""file0"",""name"":""Reference Sheet"",""type"":""document""}]",8,Tue,9,Tue,10,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 8.2 - Circle Moving 2,8.2P,Recreate your circle moving program using C,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",8,Tue,9,Tue,10,Mon,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Test 2,T2,Covers all core concepts.,1,0,TRUE,[],9,Fri,9,Fri,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 9.1 - Reading Another Language,9.1P,Demonstrate how programs written in C work within the computer,2,0,FALSE,"[{""key"":""file0"",""name"":""Execution of Program 1"",""type"":""image""},{""key"":""file1"",""name"":""Execution of Program 2"",""type"":""image""}]",9,Tue,10,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Credit Task 9.2 - Another Language,9.2C,Create a program with C using the concepts covered.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",9,Tue,10,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +High Distinction Task 10.1 - Custom Program,10.1H,Extend your custom program to meet the High Distinction criteria.,4,3,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Design overview"",""type"":""document""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",10,Tue,13,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +High Distinction Task 10.2 - Research Report,10.2H,Start working on a research project,8,3,FALSE,"[{""key"":""file0"",""name"":""Research Report Document"",""type"":""document""}]",10,Tue,13,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Pass Task 11.1 - Learning Summary Report,11.1P,Summarise your learning from the unit.,4,0,FALSE,"[{""key"":""file0"",""name"":""Learning Summary Report"",""type"":""document""}]",11,Tue,12,Tue,,,0,FALSE,90,,,import-tasks,,,,FALSE,[],[] +Distinction Task 6.2 - Custom Program,6.2D,Start working on your custom program!,16,2,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Design overview"",""type"":""document""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",6,Tue,13,Tue,,,5,TRUE,90,,,import-tasks,,,,FALSE,[],[] diff --git a/test_files/COS10001-ImportTasksWithoutTutorialStream.csv b/test_files/COS10001-ImportTasksWithoutTutorialStream.csv index e0d553332..a5e4a8970 100644 --- a/test_files/COS10001-ImportTasksWithoutTutorialStream.csv +++ b/test_files/COS10001-ImportTasksWithoutTutorialStream.csv @@ -1,37 +1,37 @@ -name,abbreviation,description,weighting,target_grade,restrict_status_updates,upload_requirements,start_week,start_day,target_week,target_day,due_week,due_day,max_quality_pts,is_graded,plagiarism_warn_pct,plagiarism_checks,group_set,tutorial_stream,scorm_enabled,scorm_allow_review,scorm_bypass_test,scorm_time_delay_enabled,scorm_attempt_limit,assess_in_portfolio_only,task_prerequisites -Pass Task 1.1 - Hello World,1.1P,"As a first step, create the classic 'Hello World' program. This will help ensure that you have all of the software installed correctly, and are ready to move on with creating other,,, programs.",1,0,FALSE,"[{""key"":""file0"",""name"":""HelloWorld.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 1.2 - Picture Drawing,1.2P,Create a program that calls procedures to draw a picture to a window (something other than a house which we use as the example).,2,0,FALSE,"[{""key"":""file0"",""name"":""PictureDrawing.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 1.3 - Creating a Procedure,1.3P,"Now that you have created a program that uses procedures, you can learn how to create your own procedures. Creating procedures will allow you to group your program's actions into procedures that perform meaningful tasks.",2,0,FALSE,"[{""key"":""file0"",""name"":""PictureDrawing.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,,,,,FALSE,[] -Credit Task 1.4 - Concept Map,1.4C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",1,Tue,2,Tue,,,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 2.1 - Hand Execute Assignment,2.1P,"Using the assignment statement, you can assign a value to a variable. In this task you will demonstrate how this action works within the computer.",2,0,FALSE,"[{""key"":""file0"",""name"":""Program Execution 1"",""type"":""image""},{""key"":""file1"",""name"":""Program Execution 2"",""type"":""image""},{""key"":""file2"",""name"":""Program Execution 3"",""type"":""image""},{""key"":""file3"",""name"":""Program Execution 4"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 2.2 - Hello User,2.2P,Now that we have variables we can create a program that reads in the users name from the Terminal and echoes back a welcome message.,4,0,FALSE,"[{""key"":""file0"",""name"":""HelloUser.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 2.3 - My Drawing Procedure,2.3P,Procedures are a great way of encapsulating the instructions needed to perform a task. In most cases the task will need some input data for it to work with. Use parameters to provide data to your procedures.,2,0,FALSE,"[{""key"":""file0"",""name"":""Shape Drawing Code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 2.4 - My Functions,2.4P,Using functions you can now create artefacts to encapsulate the steps needed to calculate a value.,4,0,FALSE,"[{""key"":""file0"",""name"":""My Function Code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,,,,,FALSE,[] -Credit Task 2.5 - Concept Maps,2.5C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",2,Tue,3,Tue,,,5,FALSE,90,,,,,,,FALSE,[] -Pass Task 3.1 - Hand Execution of Control Flow,3.1P,In this task you will use the hand execution process to demonstrate how the control flow constructs operate within the computer.,2,0,FALSE,"[{""key"":""file0"",""name"":""Button Code"",""type"":""code""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 3.2 - Name Tester,3.2P,Control flow enables you to easily add conditions and loops to your programs. In this task you will create a small program that uses conditions and loops to output custom messages to users.,4,0,FALSE,"[{""key"":""file0"",""name"":""Name Tester code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 3.3 - Circle Moving,3.3P,In this task you will create a small program that allows the user to move a circle around on the screen.,4,0,FALSE,"[{""key"":""file0"",""name"":""Circle Mover code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,,,,,FALSE,[] -Credit Task 3.4 - User Input Functions,3.4C,So far we have provided you with a unit to read and check values entered by the user: the Terminal User Input unit. In this task you will extend this library so that it has a number of additional functions.,4,1,FALSE,"[{""key"":""file0"",""name"":""User Input unit code"",""type"":""code""},{""key"":""file1"",""name"":""Program code"",""type"":""code""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,,,0,FALSE,90,,,,,,,FALSE,[] -Credit Task 3.5 - Concept Map,3.5C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",3,Tue,4,Tue,,,0,FALSE,90,,,,,,,FALSE,[] -Distinction Task 3.6 - Mandelbrot,3.6D,The Mandelbrot provides an interesting challenge in order to determine how to zoom in to and out of the section of the Mandelbrot being shown to the user.,4,2,FALSE,"[{""key"":""file0"",""name"":""Mandelbrot code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,,,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 4.1 - Using Records and Enumerations,4.1P,Effectively organising your data makes programs much easier to develop. By using records and enumerations you can start to model the entities associated with your programs.,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",4,Tue,5,Tue,7,Mon,0,FALSE,90,,,,,,,FALSE,[] -Credit Task 4.2 - Fruit Punch,4.2C,Create a program using the concepts covered so far.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",4,Tue,5,Tue,,,0,FALSE,90,,,,,,,FALSE,[] -Credit Task 4.3 - Concept Map,4.3C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",4,Tue,5,Tue,,,0,FALSE,90,,,,,,,FALSE,[] -Test 1,T1,Test 1 covers weeks 1 to 3,1,0,TRUE,[],5,Fri,5,Fri,,,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 5.1 - Hand Execution of Arrays,5.1P,Demonstrate how arrays work within the computer.,2,0,FALSE,"[{""key"":""file0"",""name"":""Execution of Program 1"",""type"":""image""},{""key"":""file1"",""name"":""Execution of Program 2"",""type"":""image""}]",5,Tue,6,Tue,10,Mon,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 5.2 - Arrays of Records,5.2P,Add an array of records to your program that uses records.,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,10,Mon,0,FALSE,90,,,,,,,FALSE,[] -Credit Task 5.3 - Food Hunter,5.3C,Extend a small game to make use of arrays.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,,,0,FALSE,90,,,,,,,FALSE,[] -Credit Task 5.4 - Concept Map,5.4C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",5,Tue,6,Tue,,,0,FALSE,90,,,,,,,FALSE,[] -Distinction Task 5.5 - Sort Visualiser,5.5D,Create a program to demonstrate sorting working within the computer.,4,2,FALSE,"[{""key"":""file0"",""name"":""Sort Visualiser"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,,,0,FALSE,90,,,,,,,FALSE,"[{""abbreviation"":""1.4C"",""task_status_id"":2},{""abbreviation"":""3.3P"",""task_status_id"":9}]" -Pass Task 6.1 - Structure Charts,6.1P,Illustrate the structure of your program using a structure chart.,2,0,FALSE,"[{""key"":""file0"",""name"":""Program structrue chart"",""type"":""image""}]",6,Tue,7,Tue,10,Mon,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 7.1 - Programming Principles,7.1P,"Describe the principles of structured, procedural, programming.",4,0,FALSE,"[{""key"":""file0"",""name"":""Program Principles Description"",""type"":""document""}]",7,Tue,8,Tue,10,Mon,0,FALSE,90,,,,,,,FALSE,[] -Distinction Task 7.2 - Game of Life,7.2D,Create the Game of Life,4,2,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",7,Tue,8,Tue,,,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 8.1 - Language Reference Sheet,8.1P,Create a reference sheet for C or C#,4,0,FALSE,"[{""key"":""file0"",""name"":""Reference Sheet"",""type"":""document""}]",8,Tue,9,Tue,10,Mon,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 8.2 - Circle Moving 2,8.2P,Recreate your circle moving program using C,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",8,Tue,9,Tue,10,Mon,0,FALSE,90,,,,,,,FALSE,[] -Test 2,T2,Covers all core concepts.,1,0,TRUE,[],9,Fri,9,Fri,,,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 9.1 - Reading Another Language,9.1P,Demonstrate how programs written in C work within the computer,2,0,FALSE,"[{""key"":""file0"",""name"":""Execution of Program 1"",""type"":""image""},{""key"":""file1"",""name"":""Execution of Program 2"",""type"":""image""}]",9,Tue,10,Tue,,,0,FALSE,90,,,,,,,FALSE,[] -Credit Task 9.2 - Another Language,9.2C,Create a program with C using the concepts covered.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",9,Tue,10,Tue,,,0,FALSE,90,,,,,,,FALSE,[] -High Distinction Task 10.1 - Custom Program,10.1H,Extend your custom program to meet the High Distinction criteria.,4,3,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Design overview"",""type"":""document""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",10,Tue,13,Tue,,,0,FALSE,90,,,,,,,FALSE,[] -High Distinction Task 10.2 - Research Report,10.2H,Start working on a research project,8,3,FALSE,"[{""key"":""file0"",""name"":""Research Report Document"",""type"":""document""}]",10,Tue,13,Tue,,,0,FALSE,90,,,,,,,FALSE,[] -Pass Task 11.1 - Learning Summary Report,11.1P,Summarise your learning from the unit.,4,0,FALSE,"[{""key"":""file0"",""name"":""Learning Summary Report"",""type"":""document""}]",11,Tue,12,Tue,,,0,FALSE,90,,,,,,,FALSE,[] -Distinction Task 6.2 - Custom Program,6.2D,Start working on your custom program!,16,2,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Design overview"",""type"":""document""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",6,Tue,13,Tue,,,5,TRUE,90,,,,,,,FALSE,[] +name,abbreviation,description,weighting,target_grade,restrict_status_updates,upload_requirements,start_week,start_day,target_week,target_day,due_week,due_day,max_quality_pts,is_graded,plagiarism_warn_pct,plagiarism_checks,group_set,tutorial_stream,scorm_enabled,scorm_allow_review,scorm_bypass_test,scorm_time_delay_enabled,scorm_attempt_limit,assess_in_portfolio_only,task_prerequisites,discussion_prompts +Pass Task 1.1 - Hello World,1.1P,"As a first step, create the classic 'Hello World' program. This will help ensure that you have all of the software installed correctly, and are ready to move on with creating other,,, programs.",1,0,FALSE,"[{""key"":""file0"",""name"":""HelloWorld.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 1.2 - Picture Drawing,1.2P,Create a program that calls procedures to draw a picture to a window (something other than a house which we use as the example).,2,0,FALSE,"[{""key"":""file0"",""name"":""PictureDrawing.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 1.3 - Creating a Procedure,1.3P,"Now that you have created a program that uses procedures, you can learn how to create your own procedures. Creating procedures will allow you to group your program's actions into procedures that perform meaningful tasks.",2,0,FALSE,"[{""key"":""file0"",""name"":""PictureDrawing.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Credit Task 1.4 - Concept Map,1.4C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",1,Tue,2,Tue,,,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 2.1 - Hand Execute Assignment,2.1P,"Using the assignment statement, you can assign a value to a variable. In this task you will demonstrate how this action works within the computer.",2,0,FALSE,"[{""key"":""file0"",""name"":""Program Execution 1"",""type"":""image""},{""key"":""file1"",""name"":""Program Execution 2"",""type"":""image""},{""key"":""file2"",""name"":""Program Execution 3"",""type"":""image""},{""key"":""file3"",""name"":""Program Execution 4"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 2.2 - Hello User,2.2P,Now that we have variables we can create a program that reads in the users name from the Terminal and echoes back a welcome message.,4,0,FALSE,"[{""key"":""file0"",""name"":""HelloUser.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 2.3 - My Drawing Procedure,2.3P,Procedures are a great way of encapsulating the instructions needed to perform a task. In most cases the task will need some input data for it to work with. Use parameters to provide data to your procedures.,2,0,FALSE,"[{""key"":""file0"",""name"":""Shape Drawing Code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 2.4 - My Functions,2.4P,Using functions you can now create artefacts to encapsulate the steps needed to calculate a value.,4,0,FALSE,"[{""key"":""file0"",""name"":""My Function Code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Credit Task 2.5 - Concept Maps,2.5C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",2,Tue,3,Tue,,,5,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 3.1 - Hand Execution of Control Flow,3.1P,In this task you will use the hand execution process to demonstrate how the control flow constructs operate within the computer.,2,0,FALSE,"[{""key"":""file0"",""name"":""Button Code"",""type"":""code""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 3.2 - Name Tester,3.2P,Control flow enables you to easily add conditions and loops to your programs. In this task you will create a small program that uses conditions and loops to output custom messages to users.,4,0,FALSE,"[{""key"":""file0"",""name"":""Name Tester code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 3.3 - Circle Moving,3.3P,In this task you will create a small program that allows the user to move a circle around on the screen.,4,0,FALSE,"[{""key"":""file0"",""name"":""Circle Mover code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Credit Task 3.4 - User Input Functions,3.4C,So far we have provided you with a unit to read and check values entered by the user: the Terminal User Input unit. In this task you will extend this library so that it has a number of additional functions.,4,1,FALSE,"[{""key"":""file0"",""name"":""User Input unit code"",""type"":""code""},{""key"":""file1"",""name"":""Program code"",""type"":""code""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,,,0,FALSE,90,,,,,,,FALSE,[],[] +Credit Task 3.5 - Concept Map,3.5C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",3,Tue,4,Tue,,,0,FALSE,90,,,,,,,FALSE,[],[] +Distinction Task 3.6 - Mandelbrot,3.6D,The Mandelbrot provides an interesting challenge in order to determine how to zoom in to and out of the section of the Mandelbrot being shown to the user.,4,2,FALSE,"[{""key"":""file0"",""name"":""Mandelbrot code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,,,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 4.1 - Using Records and Enumerations,4.1P,Effectively organising your data makes programs much easier to develop. By using records and enumerations you can start to model the entities associated with your programs.,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",4,Tue,5,Tue,7,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Credit Task 4.2 - Fruit Punch,4.2C,Create a program using the concepts covered so far.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",4,Tue,5,Tue,,,0,FALSE,90,,,,,,,FALSE,[],[] +Credit Task 4.3 - Concept Map,4.3C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",4,Tue,5,Tue,,,0,FALSE,90,,,,,,,FALSE,[],[] +Test 1,T1,Test 1 covers weeks 1 to 3,1,0,TRUE,[],5,Fri,5,Fri,,,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 5.1 - Hand Execution of Arrays,5.1P,Demonstrate how arrays work within the computer.,2,0,FALSE,"[{""key"":""file0"",""name"":""Execution of Program 1"",""type"":""image""},{""key"":""file1"",""name"":""Execution of Program 2"",""type"":""image""}]",5,Tue,6,Tue,10,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 5.2 - Arrays of Records,5.2P,Add an array of records to your program that uses records.,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,10,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Credit Task 5.3 - Food Hunter,5.3C,Extend a small game to make use of arrays.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,,,0,FALSE,90,,,,,,,FALSE,[],[] +Credit Task 5.4 - Concept Map,5.4C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",5,Tue,6,Tue,,,0,FALSE,90,,,,,,,FALSE,[],[] +Distinction Task 5.5 - Sort Visualiser,5.5D,Create a program to demonstrate sorting working within the computer.,4,2,FALSE,"[{""key"":""file0"",""name"":""Sort Visualiser"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,,,0,FALSE,90,,,,,,,FALSE,"[{""abbreviation"":""1.4C"",""task_status_id"":2},{""abbreviation"":""3.3P"",""task_status_id"":9}]",[] +Pass Task 6.1 - Structure Charts,6.1P,Illustrate the structure of your program using a structure chart.,2,0,FALSE,"[{""key"":""file0"",""name"":""Program structrue chart"",""type"":""image""}]",6,Tue,7,Tue,10,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 7.1 - Programming Principles,7.1P,"Describe the principles of structured, procedural, programming.",4,0,FALSE,"[{""key"":""file0"",""name"":""Program Principles Description"",""type"":""document""}]",7,Tue,8,Tue,10,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Distinction Task 7.2 - Game of Life,7.2D,Create the Game of Life,4,2,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",7,Tue,8,Tue,,,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 8.1 - Language Reference Sheet,8.1P,Create a reference sheet for C or C#,4,0,FALSE,"[{""key"":""file0"",""name"":""Reference Sheet"",""type"":""document""}]",8,Tue,9,Tue,10,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 8.2 - Circle Moving 2,8.2P,Recreate your circle moving program using C,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",8,Tue,9,Tue,10,Mon,0,FALSE,90,,,,,,,FALSE,[],[] +Test 2,T2,Covers all core concepts.,1,0,TRUE,[],9,Fri,9,Fri,,,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 9.1 - Reading Another Language,9.1P,Demonstrate how programs written in C work within the computer,2,0,FALSE,"[{""key"":""file0"",""name"":""Execution of Program 1"",""type"":""image""},{""key"":""file1"",""name"":""Execution of Program 2"",""type"":""image""}]",9,Tue,10,Tue,,,0,FALSE,90,,,,,,,FALSE,[],[] +Credit Task 9.2 - Another Language,9.2C,Create a program with C using the concepts covered.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",9,Tue,10,Tue,,,0,FALSE,90,,,,,,,FALSE,[],[] +High Distinction Task 10.1 - Custom Program,10.1H,Extend your custom program to meet the High Distinction criteria.,4,3,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Design overview"",""type"":""document""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",10,Tue,13,Tue,,,0,FALSE,90,,,,,,,FALSE,[],[] +High Distinction Task 10.2 - Research Report,10.2H,Start working on a research project,8,3,FALSE,"[{""key"":""file0"",""name"":""Research Report Document"",""type"":""document""}]",10,Tue,13,Tue,,,0,FALSE,90,,,,,,,FALSE,[],[] +Pass Task 11.1 - Learning Summary Report,11.1P,Summarise your learning from the unit.,4,0,FALSE,"[{""key"":""file0"",""name"":""Learning Summary Report"",""type"":""document""}]",11,Tue,12,Tue,,,0,FALSE,90,,,,,,,FALSE,[],[] +Distinction Task 6.2 - Custom Program,6.2D,Start working on your custom program!,16,2,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Design overview"",""type"":""document""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",6,Tue,13,Tue,,,5,TRUE,90,,,,,,,FALSE,[],[] diff --git a/test_files/COS10001-Tasks.csv b/test_files/COS10001-Tasks.csv index 3845398d8..54036fe3c 100644 --- a/test_files/COS10001-Tasks.csv +++ b/test_files/COS10001-Tasks.csv @@ -1,38 +1,38 @@ -name,abbreviation,description,weighting,target_grade,restrict_status_updates,upload_requirements,start_week,start_day,target_week,target_day,due_week,due_day,max_quality_pts,is_graded,plagiarism_warn_pct,plagiarism_checks,group_set,tutorial_stream,scorm_enabled,scorm_allow_review,scorm_bypass_test,scorm_time_delay_enabled,scorm_attempt_limit,assess_in_portfolio_only,task_prerequisites -Pass Task 1.1 - Hello World,1.1P,"As a first step, create the classic 'Hello World' program. This will help ensure that you have all of the software installed correctly, and are ready to move on with creating other,,, programs.",1,0,FALSE,"[{""key"":""file0"",""name"":""HelloWorld.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 1.2 - Picture Drawing,1.2P,Create a program that calls procedures to draw a picture to a window (something other than a house which we use as the example).,2,0,FALSE,"[{""key"":""file0"",""name"":""PictureDrawing.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 1.3 - Creating a Procedure,1.3P,"Now that you have created a program that uses procedures, you can learn how to create your own procedures. Creating procedures will allow you to group your program's actions into procedures that perform meaningful tasks.",2,0,FALSE,"[{""key"":""file0"",""name"":""PictureDrawing.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Credit Task 1.4 - Concept Map,1.4C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",1,Tue,2,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 2.1 - Hand Execute Assignment,2.1P,"Using the assignment statement, you can assign a value to a variable. In this task you will demonstrate how this action works within the computer.",2,0,FALSE,"[{""key"":""file0"",""name"":""Program Execution 1"",""type"":""image""},{""key"":""file1"",""name"":""Program Execution 2"",""type"":""image""},{""key"":""file2"",""name"":""Program Execution 3"",""type"":""image""},{""key"":""file3"",""name"":""Program Execution 4"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 2.2 - Hello User,2.2P,Now that we have variables we can create a program that reads in the users name from the Terminal and echoes back a welcome message.,4,0,FALSE,"[{""key"":""file0"",""name"":""HelloUser.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 2.3 - My Drawing Procedure,2.3P,Procedures are a great way of encapsulating the instructions needed to perform a task. In most cases the task will need some input data for it to work with. Use parameters to provide data to your procedures.,2,0,FALSE,"[{""key"":""file0"",""name"":""Shape Drawing Code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 2.4 - My Functions,2.4P,Using functions you can now create artefacts to encapsulate the steps needed to calculate a value.,4,0,FALSE,"[{""key"":""file0"",""name"":""My Function Code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Credit Task 2.5 - Concept Maps,2.5C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",2,Tue,3,Tue,,,5,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 3.1 - Hand Execution of Control Flow,3.1P,In this task you will use the hand execution process to demonstrate how the control flow constructs operate within the computer.,2,0,FALSE,"[{""key"":""file0"",""name"":""Button Code"",""type"":""code""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 3.2 - Name Tester,3.2P,Control flow enables you to easily add conditions and loops to your programs. In this task you will create a small program that uses conditions and loops to output custom messages to users.,4,0,FALSE,"[{""key"":""file0"",""name"":""Name Tester code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 3.3 - Circle Moving,3.3P,In this task you will create a small program that allows the user to move a circle around on the screen.,4,0,FALSE,"[{""key"":""file0"",""name"":""Circle Mover code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Credit Task 3.4 - User Input Functions,3.4C,So far we have provided you with a unit to read and check values entered by the user: the Terminal User Input unit. In this task you will extend this library so that it has a number of additional functions.,4,1,FALSE,"[{""key"":""file0"",""name"":""User Input unit code"",""type"":""code""},{""key"":""file1"",""name"":""Program code"",""type"":""code""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[] -Credit Task 3.5 - Concept Map,3.5C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",3,Tue,4,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[] -Distinction Task 3.6 - Mandelbrot,3.6D,The Mandelbrot provides an interesting challenge in order to determine how to zoom in to and out of the section of the Mandelbrot being shown to the user.,4,2,FALSE,"[{""key"":""file0"",""name"":""Mandelbrot code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 4.1 - Using Records and Enumerations,4.1P,Effectively organising your data makes programs much easier to develop. By using records and enumerations you can start to model the entities associated with your programs.,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",4,Tue,5,Tue,7,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Credit Task 4.2 - Fruit Punch,4.2C,Create a program using the concepts covered so far.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",4,Tue,5,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[] -Credit Task 4.3 - Concept Map,4.3C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",4,Tue,5,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[] -Test 1,T1,Test 1 covers weeks 1 to 3,1,0,TRUE,[],5,Fri,5,Fri,,,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 5.1 - Hand Execution of Arrays,5.1P,Demonstrate how arrays work within the computer.,2,0,FALSE,"[{""key"":""file0"",""name"":""Execution of Program 1"",""type"":""image""},{""key"":""file1"",""name"":""Execution of Program 2"",""type"":""image""}]",5,Tue,6,Tue,10,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 5.2 - Arrays of Records,5.2P,Add an array of records to your program that uses records.,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,10,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Credit Task 5.3 - Food Hunter,5.3C,Extend a small game to make use of arrays.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[] -Credit Task 5.4 - Concept Map,5.4C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",5,Tue,6,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[] -Distinction Task 5.5 - Sort Visualiser,5.5D,Create a program to demonstrate sorting working within the computer.,4,2,FALSE,"[{""key"":""file0"",""name"":""Sort Visualiser"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,,,0,FALSE,90,,,import-tasks,TRUE,"[{""abbreviation"":""1.4C"",""task_status_id"":2},{""abbreviation"":""3.3P"",""task_status_id"":9}]" -Pass Task 6.1 - Structure Charts,6.1P,Illustrate the structure of your program using a structure chart.,2,0,FALSE,"[{""key"":""file0"",""name"":""Program structrue chart"",""type"":""image""}]",6,Tue,7,Tue,10,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 7.1 - Programming Principles,7.1P,"Describe the principles of structured, procedural, programming.",4,0,FALSE,"[{""key"":""file0"",""name"":""Program Principles Description"",""type"":""document""}]",7,Tue,8,Tue,10,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Distinction Task 7.2 - Game of Life,7.2D,Create the Game of Life,4,2,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",7,Tue,8,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 8.1 - Language Reference Sheet,8.1P,Create a reference sheet for C or C#,4,0,FALSE,"[{""key"":""file0"",""name"":""Reference Sheet"",""type"":""document""}]",8,Tue,9,Tue,10,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 8.2 - Circle Moving 2,8.2P,Recreate your circle moving program using C,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",8,Tue,9,Tue,10,Mon,0,FALSE,90,,,import-tasks,FALSE,[] -Test 2,T2,Covers all core concepts.,1,0,TRUE,[],9,Fri,9,Fri,,,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 9.1 - Reading Another Language,9.1P,Demonstrate how programs written in C work within the computer,2,0,FALSE,"[{""key"":""file0"",""name"":""Execution of Program 1"",""type"":""image""},{""key"":""file1"",""name"":""Execution of Program 2"",""type"":""image""}]",9,Tue,10,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[] -Credit Task 9.2 - Another Language,9.2C,Create a program with C using the concepts covered.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",9,Tue,10,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[] -High Distinction Task 10.1 - Custom Program,10.1H,Extend your custom program to meet the High Distinction criteria.,4,3,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Design overview"",""type"":""document""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",10,Tue,13,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[] -High Distinction Task 10.2 - Research Report,10.2H,Start working on a research project,8,3,FALSE,"[{""key"":""file0"",""name"":""Research Report Document"",""type"":""document""}]",10,Tue,13,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[] -Pass Task 11.1 - Learning Summary Report,11.1P,Summarise your learning from the unit.,4,0,FALSE,"[{""key"":""file0"",""name"":""Learning Summary Report"",""type"":""document""}]",11,Tue,12,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[] -Distinction Task 6.2 - Custom Program,6.2D,Start working on your custom program!,16,2,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Design overview"",""type"":""document""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",6,Tue,13,Tue,,,5,TRUE,90,,,import-tasks,FALSE,[] -Test 10,T10,Test 10 tests the import task bug,1,0,TRUE,[],10,Fri,10,Fri,,,0,FALSE,90,,,import-tasks,FALSE,[] +name,abbreviation,description,weighting,target_grade,restrict_status_updates,upload_requirements,start_week,start_day,target_week,target_day,due_week,due_day,max_quality_pts,is_graded,plagiarism_warn_pct,plagiarism_checks,group_set,tutorial_stream,scorm_enabled,scorm_allow_review,scorm_bypass_test,scorm_time_delay_enabled,scorm_attempt_limit,assess_in_portfolio_only,task_prerequisites,discussion_prompts +Pass Task 1.1 - Hello World,1.1P,"As a first step, create the classic 'Hello World' program. This will help ensure that you have all of the software installed correctly, and are ready to move on with creating other,,, programs.",1,0,FALSE,"[{""key"":""file0"",""name"":""HelloWorld.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 1.2 - Picture Drawing,1.2P,Create a program that calls procedures to draw a picture to a window (something other than a house which we use as the example).,2,0,FALSE,"[{""key"":""file0"",""name"":""PictureDrawing.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 1.3 - Creating a Procedure,1.3P,"Now that you have created a program that uses procedures, you can learn how to create your own procedures. Creating procedures will allow you to group your program's actions into procedures that perform meaningful tasks.",2,0,FALSE,"[{""key"":""file0"",""name"":""PictureDrawing.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",1,Tue,2,Tue,5,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Credit Task 1.4 - Concept Map,1.4C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",1,Tue,2,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 2.1 - Hand Execute Assignment,2.1P,"Using the assignment statement, you can assign a value to a variable. In this task you will demonstrate how this action works within the computer.",2,0,FALSE,"[{""key"":""file0"",""name"":""Program Execution 1"",""type"":""image""},{""key"":""file1"",""name"":""Program Execution 2"",""type"":""image""},{""key"":""file2"",""name"":""Program Execution 3"",""type"":""image""},{""key"":""file3"",""name"":""Program Execution 4"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 2.2 - Hello User,2.2P,Now that we have variables we can create a program that reads in the users name from the Terminal and echoes back a welcome message.,4,0,FALSE,"[{""key"":""file0"",""name"":""HelloUser.pas"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 2.3 - My Drawing Procedure,2.3P,Procedures are a great way of encapsulating the instructions needed to perform a task. In most cases the task will need some input data for it to work with. Use parameters to provide data to your procedures.,2,0,FALSE,"[{""key"":""file0"",""name"":""Shape Drawing Code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 2.4 - My Functions,2.4P,Using functions you can now create artefacts to encapsulate the steps needed to calculate a value.,4,0,FALSE,"[{""key"":""file0"",""name"":""My Function Code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",2,Tue,3,Tue,5,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Credit Task 2.5 - Concept Maps,2.5C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",2,Tue,3,Tue,,,5,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 3.1 - Hand Execution of Control Flow,3.1P,In this task you will use the hand execution process to demonstrate how the control flow constructs operate within the computer.,2,0,FALSE,"[{""key"":""file0"",""name"":""Button Code"",""type"":""code""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 3.2 - Name Tester,3.2P,Control flow enables you to easily add conditions and loops to your programs. In this task you will create a small program that uses conditions and loops to output custom messages to users.,4,0,FALSE,"[{""key"":""file0"",""name"":""Name Tester code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 3.3 - Circle Moving,3.3P,In this task you will create a small program that allows the user to move a circle around on the screen.,4,0,FALSE,"[{""key"":""file0"",""name"":""Circle Mover code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,7,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Credit Task 3.4 - User Input Functions,3.4C,So far we have provided you with a unit to read and check values entered by the user: the Terminal User Input unit. In this task you will extend this library so that it has a number of additional functions.,4,1,FALSE,"[{""key"":""file0"",""name"":""User Input unit code"",""type"":""code""},{""key"":""file1"",""name"":""Program code"",""type"":""code""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +Credit Task 3.5 - Concept Map,3.5C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",3,Tue,4,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +Distinction Task 3.6 - Mandelbrot,3.6D,The Mandelbrot provides an interesting challenge in order to determine how to zoom in to and out of the section of the Mandelbrot being shown to the user.,4,2,FALSE,"[{""key"":""file0"",""name"":""Mandelbrot code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",3,Tue,4,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 4.1 - Using Records and Enumerations,4.1P,Effectively organising your data makes programs much easier to develop. By using records and enumerations you can start to model the entities associated with your programs.,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",4,Tue,5,Tue,7,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Credit Task 4.2 - Fruit Punch,4.2C,Create a program using the concepts covered so far.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",4,Tue,5,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +Credit Task 4.3 - Concept Map,4.3C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",4,Tue,5,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +Test 1,T1,Test 1 covers weeks 1 to 3,1,0,TRUE,[],5,Fri,5,Fri,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 5.1 - Hand Execution of Arrays,5.1P,Demonstrate how arrays work within the computer.,2,0,FALSE,"[{""key"":""file0"",""name"":""Execution of Program 1"",""type"":""image""},{""key"":""file1"",""name"":""Execution of Program 2"",""type"":""image""}]",5,Tue,6,Tue,10,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 5.2 - Arrays of Records,5.2P,Add an array of records to your program that uses records.,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,10,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Credit Task 5.3 - Food Hunter,5.3C,Extend a small game to make use of arrays.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +Credit Task 5.4 - Concept Map,5.4C,A concept map visually shows the relationships between concepts. This task aims to help you think through the various relationships between the structured procedural programming concepts and the associated programming artefacts.,4,1,FALSE,"[{""key"":""file0"",""name"":""Concept map"",""type"":""document""}]",5,Tue,6,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +Distinction Task 5.5 - Sort Visualiser,5.5D,Create a program to demonstrate sorting working within the computer.,4,2,FALSE,"[{""key"":""file0"",""name"":""Sort Visualiser"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",5,Tue,6,Tue,,,0,FALSE,90,,,import-tasks,TRUE,"[{""abbreviation"":""1.4C"",""task_status_id"":2},{""abbreviation"":""3.3P"",""task_status_id"":9}]",[] +Pass Task 6.1 - Structure Charts,6.1P,Illustrate the structure of your program using a structure chart.,2,0,FALSE,"[{""key"":""file0"",""name"":""Program structrue chart"",""type"":""image""}]",6,Tue,7,Tue,10,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 7.1 - Programming Principles,7.1P,"Describe the principles of structured, procedural, programming.",4,0,FALSE,"[{""key"":""file0"",""name"":""Program Principles Description"",""type"":""document""}]",7,Tue,8,Tue,10,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Distinction Task 7.2 - Game of Life,7.2D,Create the Game of Life,4,2,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",7,Tue,8,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 8.1 - Language Reference Sheet,8.1P,Create a reference sheet for C or C#,4,0,FALSE,"[{""key"":""file0"",""name"":""Reference Sheet"",""type"":""document""}]",8,Tue,9,Tue,10,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 8.2 - Circle Moving 2,8.2P,Recreate your circle moving program using C,4,0,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",8,Tue,9,Tue,10,Mon,0,FALSE,90,,,import-tasks,FALSE,[],[] +Test 2,T2,Covers all core concepts.,1,0,TRUE,[],9,Fri,9,Fri,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 9.1 - Reading Another Language,9.1P,Demonstrate how programs written in C work within the computer,2,0,FALSE,"[{""key"":""file0"",""name"":""Execution of Program 1"",""type"":""image""},{""key"":""file1"",""name"":""Execution of Program 2"",""type"":""image""}]",9,Tue,10,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +Credit Task 9.2 - Another Language,9.2C,Create a program with C using the concepts covered.,4,1,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Screenshot"",""type"":""image""}]",9,Tue,10,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +High Distinction Task 10.1 - Custom Program,10.1H,Extend your custom program to meet the High Distinction criteria.,4,3,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Design overview"",""type"":""document""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",10,Tue,13,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +High Distinction Task 10.2 - Research Report,10.2H,Start working on a research project,8,3,FALSE,"[{""key"":""file0"",""name"":""Research Report Document"",""type"":""document""}]",10,Tue,13,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +Pass Task 11.1 - Learning Summary Report,11.1P,Summarise your learning from the unit.,4,0,FALSE,"[{""key"":""file0"",""name"":""Learning Summary Report"",""type"":""document""}]",11,Tue,12,Tue,,,0,FALSE,90,,,import-tasks,FALSE,[],[] +Distinction Task 6.2 - Custom Program,6.2D,Start working on your custom program!,16,2,FALSE,"[{""key"":""file0"",""name"":""Program code"",""type"":""code""},{""key"":""file1"",""name"":""Design overview"",""type"":""document""},{""key"":""file2"",""name"":""Screenshot"",""type"":""image""}]",6,Tue,13,Tue,,,5,TRUE,90,,,import-tasks,FALSE,[],[] +Test 10,T10,Test 10 tests the import task bug,1,0,TRUE,[],10,Fri,10,Fri,,,0,FALSE,90,,,import-tasks,FALSE,[],[] diff --git a/test_files/csv_test_files/COS10001-Tasks-Prerequisites.csv b/test_files/csv_test_files/COS10001-Tasks-Prerequisites.csv index a9e2274fd..a399ef52e 100644 --- a/test_files/csv_test_files/COS10001-Tasks-Prerequisites.csv +++ b/test_files/csv_test_files/COS10001-Tasks-Prerequisites.csv @@ -1,6 +1,6 @@ -name,abbreviation,description,weighting,target_grade,restrict_status_updates,max_quality_pts,is_graded,plagiarism_warn_pct,plagiarism_checks,group_set,upload_requirements,start_week,start_day,target_week,target_day,due_week,due_day,scorm_enabled,scorm_allow_review,scorm_bypass_test,scorm_time_delay_enabled,scorm_attempt_limit,tutorial_stream,assess_in_portfolio_only,task_prerequisites -Assignment 12,A12,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",-1,Sat,1,Mon,13,Mon,,,,,,import-tasks,FALSE,[] -Pass task 1,1.1P,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",-1,Sat,1,Mon,12,Mon,,,,,,import-tasks,FALSE,[] -Pass task 2,2.1P,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",-1,Sat,1,Mon,12,Mon,,,,,,import-tasks,FALSE,[] -Distinction Task,5.5D,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",2,Sat,3,Mon,13,Mon,,,,,,import-tasks,FALSE,[] -Distinction Task 2,5.5D.new,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",2,Sat,3,Mon,13,Mon,,,,,,import-tasks,FALSE,"[{""abbreviation"":""1.1P"",""task_status_id"":2},{""abbreviation"":""1.2P"",""task_status_id"":9}]" +name,abbreviation,description,weighting,target_grade,restrict_status_updates,max_quality_pts,is_graded,plagiarism_warn_pct,plagiarism_checks,group_set,upload_requirements,start_week,start_day,target_week,target_day,due_week,due_day,scorm_enabled,scorm_allow_review,scorm_bypass_test,scorm_time_delay_enabled,scorm_attempt_limit,tutorial_stream,assess_in_portfolio_only,task_prerequisites,discussion_prompts +Assignment 12,A12,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",-1,Sat,1,Mon,13,Mon,,,,,,import-tasks,FALSE,[],[] +Pass task 1,1.1P,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",-1,Sat,1,Mon,12,Mon,,,,,,import-tasks,FALSE,[],[] +Pass task 2,2.1P,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",-1,Sat,1,Mon,12,Mon,,,,,,import-tasks,FALSE,[],[] +Distinction Task,5.5D,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",2,Sat,3,Mon,13,Mon,,,,,,import-tasks,FALSE,[],[] +Distinction Task 2,5.5D.new,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",2,Sat,3,Mon,13,Mon,,,,,,import-tasks,FALSE,"[{""abbreviation"":""1.1P"",""task_status_id"":2},{""abbreviation"":""1.2P"",""task_status_id"":9}]",[] diff --git a/test_files/csv_test_files/COS10001-Tasks.csv b/test_files/csv_test_files/COS10001-Tasks.csv index e28c64ead..0bdc01213 100644 --- a/test_files/csv_test_files/COS10001-Tasks.csv +++ b/test_files/csv_test_files/COS10001-Tasks.csv @@ -1,5 +1,5 @@ -name,abbreviation,description,weighting,target_grade,restrict_status_updates,max_quality_pts,is_graded,plagiarism_warn_pct,plagiarism_checks,group_set,upload_requirements,start_week,start_day,target_week,target_day,due_week,due_day,scorm_enabled,scorm_allow_review,scorm_bypass_test,scorm_time_delay_enabled,scorm_attempt_limit,tutorial_stream,assess_in_portfolio_only,task_prerequisites -Assignment 12,A12,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",-1,Sat,1,Mon,13,Mon,,,,,,import-tasks,FALSE,[] -Pass task 1,1.1P,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",-1,Sat,1,Mon,12,Mon,,,,,,import-tasks,FALSE,[] -Pass task 2,2.1P,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",-1,Sat,1,Mon,12,Mon,,,,,,import-tasks,FALSE,[] -Distinction Task,5.5D,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",2,Sat,3,Mon,13,Mon,,,,,,import-tasks,FALSE,"[{""abbreviation"":""1.1P"",""task_status_id"":2},{""abbreviation"":""1.2P"",""task_status_id"":9}]" +name,abbreviation,description,weighting,target_grade,restrict_status_updates,max_quality_pts,is_graded,plagiarism_warn_pct,plagiarism_checks,group_set,upload_requirements,start_week,start_day,target_week,target_day,due_week,due_day,scorm_enabled,scorm_allow_review,scorm_bypass_test,scorm_time_delay_enabled,scorm_attempt_limit,tutorial_stream,assess_in_portfolio_only,task_prerequisites,discussion_prompts +Assignment 12,A12,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",-1,Sat,1,Mon,13,Mon,,,,,,import-tasks,FALSE,[],[] +Pass task 1,1.1P,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",-1,Sat,1,Mon,12,Mon,,,,,,import-tasks,FALSE,[],[] +Pass task 2,2.1P,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",-1,Sat,1,Mon,12,Mon,,,,,,import-tasks,FALSE,[],[] +Distinction Task,5.5D,rerum ut fugit saepe ipsa in quidem,2,0,FALSE,0,FALSE,50,[],,"[{""key"":""file0"",""name"":""Assumenda accusamus quas"",""type"":""image""}]",2,Sat,3,Mon,13,Mon,,,,,,import-tasks,FALSE,"[{""abbreviation"":""1.1P"",""task_status_id"":2},{""abbreviation"":""1.2P"",""task_status_id"":9}]",[] diff --git a/test_files/csv_test_files/COS10001-Tasks.xlsx b/test_files/csv_test_files/COS10001-Tasks.xlsx index af83229fe..dfe25c7d0 100644 Binary files a/test_files/csv_test_files/COS10001-Tasks.xlsx and b/test_files/csv_test_files/COS10001-Tasks.xlsx differ diff --git a/test_files/unit_csv_imports/import_group_tasks.csv b/test_files/unit_csv_imports/import_group_tasks.csv index d2ab3d948..2b576cfeb 100644 --- a/test_files/unit_csv_imports/import_group_tasks.csv +++ b/test_files/unit_csv_imports/import_group_tasks.csv @@ -1,3 +1,3 @@ -name,abbreviation,description,weighting,target_grade,restrict_status_updates,max_quality_pts,is_graded,plagiarism_warn_pct,plagiarism_checks,group_set,upload_requirements,start_week,start_day,target_week,target_day,due_week,due_day,tutorial_stream,scorm_enabled,scorm_allow_review,scorm_bypass_test,scorm_time_delay_enabled,scorm_attempt_limit,assess_in_portfolio_only,task_prerequisites -Group Import 1,1GI,Test Description - Import,16,0,FALSE,0,FALSE,80,[],Group Work,[],0,Mon,1,Sun,2,Wed,group-tasks-test,,,,FALSE,[] -Missing Group,2GI,Test Description - Import FAIL,16,0,FALSE,0,FALSE,80,[],Group Work1,[],0,Mon,1,Sun,2,Wed,group-tasks-test,,,,FALSE,[] +name,abbreviation,description,weighting,target_grade,restrict_status_updates,max_quality_pts,is_graded,plagiarism_warn_pct,plagiarism_checks,group_set,upload_requirements,start_week,start_day,target_week,target_day,due_week,due_day,tutorial_stream,scorm_enabled,scorm_allow_review,scorm_bypass_test,scorm_time_delay_enabled,scorm_attempt_limit,assess_in_portfolio_only,task_prerequisites,discussion_prompts +Group Import 1,1GI,Test Description - Import,16,0,FALSE,0,FALSE,80,[],Group Work,[],0,Mon,1,Sun,2,Wed,group-tasks-test,,,,FALSE,[],[] +Missing Group,2GI,Test Description - Import FAIL,16,0,FALSE,0,FALSE,80,[],Group Work1,[],0,Mon,1,Sun,2,Wed,group-tasks-test,,,,FALSE,[],[] diff --git a/vendor/assets/stylesheets/doubtfire-coverpage.css b/vendor/assets/stylesheets/doubtfire-coverpage.css index d0153fa57..ba3974885 100644 --- a/vendor/assets/stylesheets/doubtfire-coverpage.css +++ b/vendor/assets/stylesheets/doubtfire-coverpage.css @@ -76,12 +76,16 @@ button.task-status.discuss { background-color: #31b0d5; color: white; } +button.task-status.attention-required { + background-color: #f1814d; + color: white; +} button.task-status.complete { background-color: #5BB75B; background-image: linear-gradient(#62c462, #51a351); color: white; } button.task-status.assess-in-portfolio { - background-color: #91b891; + background-color: #f2d85c; color: white; }