diff --git a/bases/rsptx/admin_server_api/routers/instructor.py b/bases/rsptx/admin_server_api/routers/instructor.py index e73918e69..5faea9d84 100644 --- a/bases/rsptx/admin_server_api/routers/instructor.py +++ b/bases/rsptx/admin_server_api/routers/instructor.py @@ -16,15 +16,14 @@ import csv import re from io import StringIO -from typing import Optional -from zoneinfo import ZoneInfo # Local application imports # ------------------------- from rsptx.db.crud import ( - create_assignment_question, + term_start_utc, create_assignment, + create_assignment_question, create_course_instructor, create_course, course_attr_is_true, @@ -39,16 +38,13 @@ delete_lti_course, delete_user_course_entry, fetch_all_course_attributes, - fetch_lti1p1_config, fetch_assignment_questions, - fetch_assignments, + fetch_lti1p1_config, fetch_available_students_for_instructor_add, fetch_base_course, - fetch_course_by_id, fetch_course, fetch_courses_for_user, fetch_group, - fetch_instructor_courses, fetch_library_book, fetch_library_books, fetch_membership, @@ -56,7 +52,11 @@ fetch_question, fetch_timed_assessments, fetch_users_for_course, + import_assignment, + import_course_assignments, reset_student_assessment, + search_assignments, + search_shareable_courses, update_course_settings, update_question, update_user, @@ -65,18 +65,20 @@ from rsptx.auth.email import send_welcome_email from rsptx.templates import get_shared_templates from rsptx.validation.fields import clean_text, validate_text_field +from rsptx.validation.schemas import AssignmentsSearchRequest from rsptx.configuration import settings from rsptx.endpoint_validators import with_course, instructor_role_required from rsptx.logging import rslogger from rsptx.db.crud.user import create_user, fetch_user from rsptx.db.models import ( - AuthUserValidator, - AssignmentValidator, AssignmentQuestionValidator, + AssignmentValidator, + AuthUserValidator, CoursesValidator, ) from rsptx.response_helpers.core import canonical_utcnow, make_json_response import datetime +from typing import Optional # Routing # ======= @@ -170,6 +172,13 @@ async def get_manage_students( return templates.TemplateResponse("admin/instructor/manage_students.html", context) +# A course with more assignments than this gets its list truncated rather than +# paginated: picking one out of a list this long is not what the page is for, +# and "copy all" does not care how many there are. Capped at the search +# request's own ``limit`` ceiling, which rejects anything larger. +MAX_SOURCE_ASSIGNMENTS = 100 + + @router.get("/copy_assignments") @instructor_role_required() @with_course() @@ -181,36 +190,71 @@ async def get_copy_assignments( ): """ Display the copy assignments interface. + + The source course is chosen through ``/shareable_courses`` rather than a + dropdown rendered here: the page can now copy from any course that shares + something, which is far too many to put in a `` - - {% for course in instructor_course_list %} - - {% endfor %} - - +
+ + + + Official assignments for {{ base_course.course_name }} are listed first. + +
+ +
+ + +
+ +
+ + +
+ +
+ + -
-
-
+ @@ -53,18 +68,18 @@

Instructions

How to Copy Assignments
    -
  1. Select a source course from the dropdown
  2. -
  3. Choose which assignment to copy (or "All" for all assignments)
  4. -
  5. Click "Copy Assignment" to copy to your current course
  6. +
  7. Find the course you want to copy from
  8. +
  9. Choose one assignment, or all of them
  10. +
  11. Click "Copy"
Important Notes
diff --git a/components/rsptx/templates/admin/instructor/course_settings.html b/components/rsptx/templates/admin/instructor/course_settings.html index 437e3eb70..db3d5e2c2 100644 --- a/components/rsptx/templates/admin/instructor/course_settings.html +++ b/components/rsptx/templates/admin/instructor/course_settings.html @@ -92,6 +92,30 @@

{{ course.course_name }}

Set the default group size for peer instruction activities
+
+
+ + +
+
+ Let instructors in other courses find and import your assignments. This covers + every assignment in the course, exams included, so leave it off if any + of them should stay private. Importing never copies student work or grades, and the + instructor who imports gets their own hidden copy. +
+
+ +
+ + +
+ Shown to instructors browsing your assignments. Use it to describe your problem sets + — what they cover, how hard they are, anything worth knowing before importing one. +
+
+
diff --git a/components/rsptx/templates/admin/instructor/menu.html b/components/rsptx/templates/admin/instructor/menu.html index a945c7710..c6f56e80d 100644 --- a/components/rsptx/templates/admin/instructor/menu.html +++ b/components/rsptx/templates/admin/instructor/menu.html @@ -40,7 +40,7 @@

Grader

Copy Assignments

-

Copy assignments from previous courses

+

Copy assignments from shared courses

Configure Practice

diff --git a/components/rsptx/templates/staticAssets/css/admin.css b/components/rsptx/templates/staticAssets/css/admin.css index a65e7f628..80bd0635b 100644 --- a/components/rsptx/templates/staticAssets/css/admin.css +++ b/components/rsptx/templates/staticAssets/css/admin.css @@ -755,6 +755,89 @@ tr.duplicate { color: #333; } +/* Step 2's assignment picker is injected by JS, so the gap that keeps the Copy + button off the dropdown belongs on the container rather than on whichever + element the script happened to render last. Its label sits outside a + .form-group, so it needs the same treatment those labels get. */ +.copy-assignments .assignment-picker { + margin-bottom: 20px; +} + +.copy-assignments .assignment-picker label { + display: block; + margin-bottom: 8px; + font-weight: 500; + color: #333; +} + +/* The source-course picker. Scrolls rather than paginates: it is a shortlist to + choose from, and searching is the way through a long one. */ +.copy-assignments .course-results { + margin-top: 15px; + max-height: 22rem; + overflow-y: auto; + border: 1px solid #e0e0e0; + border-radius: 6px; +} + +.copy-assignments .course-row { + display: flex; + align-items: baseline; + flex-wrap: wrap; + gap: 8px; + width: 100%; + padding: 10px 14px; + background: none; + border: none; + border-bottom: 1px solid #eee; + text-align: left; + font: inherit; + cursor: pointer; +} + +.copy-assignments .course-row:last-child { + border-bottom: none; +} + +.copy-assignments .course-row:hover, +.copy-assignments .course-row:focus-visible { + background: #f4f8ff; +} + +.copy-assignments .course-row.selected { + background: #e8f0fe; + box-shadow: inset 3px 0 0 #1a73e8; +} + +.copy-assignments .course-row-title { + font-weight: 600; + color: #333; +} + +/* Pushed to its own line so the course name and its badges stay together. */ +.copy-assignments .course-row-meta { + flex-basis: 100%; + font-size: 13px; + color: #666; +} + +.copy-assignments .course-badge { + font-size: 11px; + font-weight: 600; + padding: 2px 7px; + border-radius: 10px; +} + +.copy-assignments .badge-primary { + background: #1a73e8; + color: #fff; +} + +.copy-assignments .badge-secondary { + background: #e0e0e0; + color: #444; +} + /* ---------- Responsive ---------- */ @media (max-width: 768px) { .admin-page { diff --git a/components/rsptx/templates/staticAssets/js/admin/copy_assignments.js b/components/rsptx/templates/staticAssets/js/admin/copy_assignments.js index 87dd793aa..2677232cd 100644 --- a/components/rsptx/templates/staticAssets/js/admin/copy_assignments.js +++ b/components/rsptx/templates/staticAssets/js/admin/copy_assignments.js @@ -1,98 +1,227 @@ -/* Copy assignments (admin/instructor/copy_assignments.html) */ +/* Copy assignments (admin/instructor/copy_assignments.html) + * + * Two steps: pick a source course, then pick one assignment or all of them. + * The course list is fetched rather than rendered into a