diff --git a/Gemfile b/Gemfile index 45367d901..c4c4be3ea 100644 --- a/Gemfile +++ b/Gemfile @@ -49,7 +49,7 @@ gem 'rake', '>=10.3.2' gem 'populator', '>=1.0.0' # To communicate with MySQL database -gem 'mysql2', '~>0.5' +gem 'mysql2', '=0.5.4' # Development server gem 'thin' diff --git a/Gemfile.lock b/Gemfile.lock index 37120e5a4..ea382c020 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -231,7 +231,7 @@ GEM momentjs-rails (2.15.1) railties (>= 3.1) multi_xml (0.6.0) - mysql2 (0.5.5) + mysql2 (0.5.4) net-http (0.4.0) uri net-imap (0.4.9) @@ -470,6 +470,7 @@ PLATFORMS arm64-darwin-21 arm64-darwin-22 arm64-darwin-23 + arm64-darwin-24 x86_64-darwin-19 x86_64-darwin-20 x86_64-darwin-21 @@ -509,7 +510,7 @@ DEPENDENCIES mini_racer (~> 0.6.3) moment_timezone-rails momentjs-rails (>= 2.9.0) - mysql2 (~> 0.5) + mysql2 (= 0.5.4) net-http net-ldap newrelic_rpm diff --git a/app/assets/javascripts/annotations.js b/app/assets/javascripts/annotations.js index 9380f2b80..6930787d6 100644 --- a/app/assets/javascripts/annotations.js +++ b/app/assets/javascripts/annotations.js @@ -27,13 +27,13 @@ $(window).on('resize', function () { function getSharedCommentsForProblem(problem_id) { return localCache['shared_comments'][problem_id]?.map( (annotation) => { - return {label: annotation.comment ?? annotation, value: annotation} + return { label: annotation.comment ?? annotation, value: annotation } } ) } const selectAnnotation = box => (e, ui) => { - const {label, value} = ui.item; + const { label, value } = ui.item; const score = value.value ?? 0; box.find('#comment-score').val(score); @@ -44,7 +44,7 @@ const selectAnnotation = box => (e, ui) => { return false; } -function focusAnnotation( event, ui ) { +function focusAnnotation(event, ui) { $(this).val(ui.item.label); return false; } @@ -191,7 +191,7 @@ function plusFix(n) { function fillAnnotationBox() { retrieveSharedComments(); $('#loadScreen').css('display', 'flex'); - $.get(document.URL, function(data) { + $.get(document.URL, function (data) { const $page = $('
').html(data); $('.problemGrades').html($page.find('.problemGrades')); $('#annotationPane').html($page.find(' #annotationPane')); @@ -382,7 +382,7 @@ function attachEvents() { $('.code-table').scrollTo($annotationLine.children().last(), { duration: "fast" }); refreshAnnotations(); } else { - M.toast({html: 'Only one annotation can be created per line at a time!'}); + M.toast({ html: 'Only one annotation can be created per line at a time!' }); } }); } @@ -461,7 +461,7 @@ function attachAnnotationPaneEvents() { }); // Chevron events (collapse / show problem) - $('.collapsible-header-controls .collapse-icon, .collapsible-header-controls .expand-icon').on('click', function(e) { + $('.collapsible-header-controls .collapse-icon, .collapsible-header-controls .expand-icon').on('click', function (e) { e.preventDefault(); if ($('#loadScreen').css('display') === 'flex') return; $(e.target).closest(".collapsible-header-wrap").find(".collapsible-header").click(); @@ -545,23 +545,31 @@ function newAnnotationFormCode() { problemGraderId[score.problem_id] = score.grader_id; }); + // Clear any existing options from the problem dropdown + var $problemSelect = box.find('select.problem-id'); + $problemSelect.empty(); + + // Add a default select option + $problemSelect.append($('')); + + // Add problems to the dropdown var processStarred = false; _.each(problems, function (problem) { if (problemGraderId[problem.id] !== 0) { // Because grader == 0 is autograder - if(problem.starred && !processStarred){ - box.find('select').append( - $('').text('Starred Problems') + if (problem.starred && !processStarred) { + box.find('select.problem-id').append( + $('').text('Starred Problems') ); - processStarred=true; + processStarred = true; } - if(!problem.starred && processStarred){ - box.find('select').append( - $('').text('-------------------') + if (!problem.starred && processStarred) { + box.find('select.problem-id').append( + $('').text('-------------------') ); - processStarred=false; + processStarred = false; } - box.find("select").append( - $("')); + + // Add rubric items for the selected problem + rubricItems.forEach(function (item) { + $rubricSelect.append( + $('').text('Starred Problems') + $('').text('Starred Problems') ); - processStarred=true; + processStarred = true; } - if(!problem.starred && processStarred){ + if (!problem.starred && processStarred) { box.find('select').append( - $('').text('-------------------') + $('').text('-------------------') ); - processStarred=false; + processStarred = false; } box.find("select").append( - $("
-
<% end %> + <% unless global %> +
+
+ + Optionally associate this annotation with a rubric item +
+
+ <% end %> +
diff --git a/app/views/submissions/_annotation_pane.html.erb b/app/views/submissions/_annotation_pane.html.erb index 37ca0f608..c72f000d4 100644 --- a/app/views/submissions/_annotation_pane.html.erb +++ b/app/views/submissions/_annotation_pane.html.erb @@ -15,17 +15,17 @@
- <% end %> + +
+ <% end %> + + + <% if problem_data[:global_annotations].any? %> +
+ <% problem_data[:global_annotations].each do |annotation| %> + <%= render partial: "annotations/global_annotation", locals: { annotation:, problem_name: problem_data[:name] } %> + <% end %> +
+ <% end %> + + + <% problem_data[:annotations_by_file].each do |filename, annotations| %> +
+ <%= clean_filename(filename) %> + <% annotations.each do |annotation| %> + <%= render partial: "annotations/line_annotation", locals: { annotation: } %> + <% end %> +
+ <% end %> <% else %> Annotations have yet to be released. <% end %> diff --git a/app/views/submissions/_golden-layout.html.erb b/app/views/submissions/_golden-layout.html.erb index 064c782a0..316267b48 100644 --- a/app/views/submissions/_golden-layout.html.erb +++ b/app/views/submissions/_golden-layout.html.erb @@ -84,7 +84,7 @@ { type: "component", componentName: "annotation_pane", - title: "Annotations", + title: "Annotations and Rubrics", height: 30, }, { diff --git a/app/views/submissions/view.html.erb b/app/views/submissions/view.html.erb index 96cd475cd..f2d1a5c32 100755 --- a/app/views/submissions/view.html.erb +++ b/app/views/submissions/view.html.erb @@ -106,6 +106,11 @@ highlightComments(); }); PDFJS.workerSrc = "<%= asset_url 'pdf.worker.js' %>"; + + var rubricItemsByProblem = {}; + <% @problems.each do |problem| %> + rubricItemsByProblem[<%= problem.id %>] = <%= raw problem.rubric_items.map { |item| { id: item.id, description: item.description, points: item.points } }.to_json %>; + <% end %> <%= render partial: "golden-layout" %> <% end %> diff --git a/config/routes.rb b/config/routes.rb index d113b57ff..fd29306ab 100755 --- a/config/routes.rb +++ b/config/routes.rb @@ -155,7 +155,12 @@ post "import", on: :collection end - resources :problems, except: [:index, :show] + # resources :problems, except: [:index, :show] + resources :problems, except: [:index, :show] do + resources :rubric_items, except: [:index, :show] do + patch :toggle_assignment, on: :member + end + end resource :scoreboard, except: [:new] resources :submissions, except: [:show] do resources :annotations, only: [:create, :update, :destroy] do @@ -166,6 +171,10 @@ resources :scores, only: [:create, :show, :update] + resources :rubric_items do + post :toggle, on: :member + end + member do get "destroyConfirm" get "download" @@ -173,6 +182,7 @@ post "release_student_grade" post "unrelease_student_grade" get "tweak_total" + get "rubric_items" end collection do diff --git a/db/migrate/20240501000000_create_rubric_item_assignments.rb b/db/migrate/20240501000000_create_rubric_item_assignments.rb new file mode 100644 index 000000000..28a3248a5 --- /dev/null +++ b/db/migrate/20240501000000_create_rubric_item_assignments.rb @@ -0,0 +1,17 @@ +class CreateRubricItemAssignments < ActiveRecord::Migration[6.1] + def change + create_table :rubric_item_assignments do |t| + t.references :rubric_item, null: false, foreign_key: true + t.references :submission, null: false, foreign_key: true + t.boolean :assigned, default: false + + t.timestamps + end + + # Add index for faster lookups + add_index :rubric_item_assignments, [:rubric_item_id, :submission_id], unique: true, name: 'index_ria_on_rubric_item_id_and_submission_id' + + # Remove assigned column from rubric_items as it will now be tracked per submission + remove_column :rubric_items, :assigned, :boolean + end +end diff --git a/db/migrate/20240501123456_add_rubric_item_id_to_annotations.rb b/db/migrate/20240501123456_add_rubric_item_id_to_annotations.rb new file mode 100644 index 000000000..7e6e28dde --- /dev/null +++ b/db/migrate/20240501123456_add_rubric_item_id_to_annotations.rb @@ -0,0 +1,5 @@ +class AddRubricItemIdToAnnotations < ActiveRecord::Migration[6.1] + def change + add_reference :annotations, :rubric_item, foreign_key: true, null: true + end +end diff --git a/db/migrate/20250426203028_create_rubric_items.rb b/db/migrate/20250426203028_create_rubric_items.rb new file mode 100644 index 000000000..52c2d83cc --- /dev/null +++ b/db/migrate/20250426203028_create_rubric_items.rb @@ -0,0 +1,13 @@ +class CreateRubricItems < ActiveRecord::Migration[6.1] + def change + create_table :rubric_items do |t| + t.references :problem, null: false, foreign_key: true, type: :integer + t.string :description, null: false + t.float :points, null: false + t.integer :order, null: false + t.timestamps + end + + add_index :rubric_items, [:problem_id, :order], unique: true + end +end \ No newline at end of file diff --git a/db/schema.rb b/db/schema.rb index b212ea0a3..7e5d7a428 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,13 +10,13 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2024_04_06_174050) do +ActiveRecord::Schema.define(version: 2025_04_26_203028) do create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false - t.bigint "record_id", null: false - t.bigint "blob_id", null: false + t.integer "record_id", null: false + t.integer "blob_id", null: false t.datetime "created_at", null: false t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true @@ -27,7 +27,7 @@ t.string "filename", null: false t.string "content_type" t.text "metadata" - t.bigint "byte_size", null: false + t.integer "byte_size", null: false t.string "checksum", null: false t.datetime "created_at", null: false t.string "service_name", null: false @@ -35,7 +35,7 @@ end create_table "active_storage_variant_records", force: :cascade do |t| - t.bigint "blob_id", null: false + t.integer "blob_id", null: false t.string "variation_digest", null: false t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true end @@ -54,6 +54,8 @@ t.string "coordinate" t.boolean "shared_comment", default: false t.boolean "global_comment", default: false + t.integer "rubric_item_id" + t.index ["rubric_item_id"], name: "index_annotations_on_rubric_item_id" end create_table "announcements", force: :cascade do |t| @@ -130,7 +132,7 @@ t.integer "course_id" t.integer "assessment_id" t.string "category_name", default: "General" - t.datetime "release_at", default: -> { "CURRENT_TIMESTAMP" } + t.datetime "release_at" t.string "slug" t.index ["assessment_id"], name: "index_attachments_on_assessment_id" t.index ["slug"], name: "index_attachments_on_slug", unique: true @@ -194,14 +196,14 @@ t.boolean "infinite", default: false, null: false end - create_table "friendly_id_slugs", charset: "utf8mb3", force: :cascade do |t| + create_table "friendly_id_slugs", force: :cascade do |t| t.string "slug", null: false t.integer "sluggable_id", null: false t.string "sluggable_type", limit: 50 t.string "scope" t.datetime "created_at" - t.index ["slug", "sluggable_type", "scope"], name: "index_friendly_id_slugs_on_slug_and_sluggable_type_and_scope", unique: true, length: { slug: 70, scope: 70 } - t.index ["slug", "sluggable_type"], name: "index_friendly_id_slugs_on_slug_and_sluggable_type", length: { slug: 140 } + t.index ["slug", "sluggable_type", "scope"], name: "index_friendly_id_slugs_on_slug_and_sluggable_type_and_scope", unique: true + t.index ["slug", "sluggable_type"], name: "index_friendly_id_slugs_on_slug_and_sluggable_type" t.index ["sluggable_type", "sluggable_id"], name: "index_friendly_id_slugs_on_sluggable_type_and_sluggable_id" end @@ -225,8 +227,8 @@ t.string "context_id" t.integer "course_id" t.datetime "last_synced" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false t.string "membership_url" t.string "platform" t.boolean "auto_sync", default: false @@ -319,6 +321,28 @@ t.integer "course_id" end + create_table "rubric_item_assignments", force: :cascade do |t| + t.integer "rubric_item_id", null: false + t.integer "submission_id", null: false + t.boolean "assigned", default: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["rubric_item_id", "submission_id"], name: "index_ria_on_rubric_item_id_and_submission_id", unique: true + t.index ["rubric_item_id"], name: "index_rubric_item_assignments_on_rubric_item_id" + t.index ["submission_id"], name: "index_rubric_item_assignments_on_submission_id" + end + + create_table "rubric_items", force: :cascade do |t| + t.integer "problem_id", null: false + t.string "description", null: false + t.float "points", null: false + t.integer "order", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["problem_id", "order"], name: "index_rubric_items_on_problem_id_and_order", unique: true + t.index ["problem_id"], name: "index_rubric_items_on_problem_id" + end + create_table "scheduler", force: :cascade do |t| t.string "action" t.datetime "next" @@ -326,7 +350,7 @@ t.integer "course_id" t.datetime "created_at" t.datetime "updated_at" - t.datetime "until", default: -> { "CURRENT_TIMESTAMP" } + t.datetime "until" t.boolean "disabled", default: false end @@ -338,15 +362,15 @@ create_table "scoreboards", force: :cascade do |t| t.integer "assessment_id" - t.text "banner" - t.text "colspec" + t.text "banner", limit: 65535 + t.text "colspec", limit: 65535 t.boolean "include_instructors", default: false end create_table "scores", force: :cascade do |t| t.integer "submission_id" t.float "score" - t.text "feedback", size: :medium + t.text "feedback", limit: 16777215 t.integer "problem_id" t.datetime "created_at" t.datetime "updated_at" @@ -372,7 +396,7 @@ t.string "submitter_ip", limit: 40 t.integer "tweak_id" t.boolean "ignored", default: false, null: false - t.string "dave" + t.string "dave", limit: 255 t.text "embedded_quiz_form_answer" t.integer "submitted_by_app_id" t.string "group_key", default: "" @@ -437,4 +461,12 @@ add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" + add_foreign_key "annotations", "rubric_items" + add_foreign_key "github_integrations", "users" + add_foreign_key "oauth_access_grants", "oauth_applications", column: "application_id" + add_foreign_key "oauth_access_tokens", "oauth_applications", column: "application_id" + add_foreign_key "oauth_device_flow_requests", "oauth_applications", column: "application_id" + add_foreign_key "rubric_item_assignments", "rubric_items" + add_foreign_key "rubric_item_assignments", "submissions" + add_foreign_key "rubric_items", "problems" end diff --git a/libsqlite3.dylib b/libsqlite3.dylib new file mode 100755 index 000000000..41e88f962 Binary files /dev/null and b/libsqlite3.dylib differ