+
+
setNewName(e.target.value)}
- placeholder="Enter new exercise name"
+ placeholder="Enter a unique name for the copy"
className="w-full"
invalid={validationMessage !== null}
/>
{validationMessage && (
-
+
)}
-
-
+ {canEditDirectly && hasEditSupport && (
+
+ )}
+
+ {!canEditDirectly && (
+
+ )}
+
+
+
diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/SearchExercises/SmartSearchExercises.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/SearchExercises/SmartSearchExercises.tsx
index ce6b190b..c4272b3e 100644
--- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/SearchExercises/SmartSearchExercises.tsx
+++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/AssignmentBuilder/components/exercises/components/SearchExercises/SmartSearchExercises.tsx
@@ -29,7 +29,15 @@ import styles from "./SmartSearchExercises.module.css";
/**
* Smart exercise search component with fixed layout and enhanced UX
*/
-export const SmartSearchExercises = () => {
+interface SmartSearchExercisesProps {
+ setCurrentEditExercise?: (exercise: Exercise | null) => void;
+ setViewMode?: (mode: "list" | "browse" | "search" | "create" | "edit") => void;
+}
+
+export const SmartSearchExercises = ({
+ setCurrentEditExercise,
+ setViewMode
+}: SmartSearchExercisesProps) => {
const dispatch = useDispatch();
const selectedExercises = useSelector(searchExercisesSelectors.getSelectedExercises);
const exerciseTypes = useSelector(datasetSelectors.getQuestionTypeOptions);
@@ -440,7 +448,8 @@ export const SmartSearchExercises = () => {
visible={copyModalVisible}
onHide={handleCopyModalHide}
exercise={selectedExerciseForCopy}
- copyToAssignment={false}
+ setCurrentEditExercise={setCurrentEditExercise}
+ setViewMode={setViewMode}
/>
);
diff --git a/bases/rsptx/assignment_server_api/routers/instructor.py b/bases/rsptx/assignment_server_api/routers/instructor.py
index 5a7bb62f..b5dc7154 100644
--- a/bases/rsptx/assignment_server_api/routers/instructor.py
+++ b/bases/rsptx/assignment_server_api/routers/instructor.py
@@ -1505,11 +1505,17 @@ async def validate_question_name(
@router.post("/copy_question")
@instructor_role_required()
+@with_course()
async def copy_question_endpoint(
- request: Request, request_data: CopyQuestionRequest, user=Depends(auth_manager)
+ request: Request,
+ request_data: CopyQuestionRequest,
+ user=Depends(auth_manager),
+ course=None,
):
"""
Copy a question with a new name and owner.
+ The user making the copy becomes the owner and author.
+ The base_course is updated to the current user's course base_course.
Optionally copy it to an assignment as well.
"""
try:
@@ -1523,6 +1529,7 @@ async def copy_question_endpoint(
new_owner=user.username,
assignment_id=assignment_id,
htmlsrc=request_data.htmlsrc,
+ new_base_course=course.base_course,
)
return make_json_response(
diff --git a/components/rsptx/db/crud/question.py b/components/rsptx/db/crud/question.py
index f099a21f..a9a1e578 100644
--- a/components/rsptx/db/crud/question.py
+++ b/components/rsptx/db/crud/question.py
@@ -744,6 +744,7 @@ async def copy_question(
new_owner: str,
assignment_id: Optional[int] = None,
htmlsrc: Optional[str] = None,
+ new_base_course: Optional[str] = None,
) -> QuestionValidator:
"""
Copy a question to create a new one with the same content but different name and owner.
@@ -753,6 +754,7 @@ async def copy_question(
:param new_owner: str, the username of the new owner
:param assignment_id: Optional[int], the assignment ID if copying to an assignment
:param htmlsrc: Optional[str], the HTML source to use for the new question (if provided, overrides original)
+ :param new_base_course: Optional[str], the base course for the new question (if provided, overrides original)
:return: QuestionValidator, the newly created question
"""
async with async_session() as session:
@@ -769,13 +771,20 @@ async def copy_question(
# Use provided htmlsrc or fall back to original
question_htmlsrc = htmlsrc if htmlsrc is not None else original_question.htmlsrc
+ # Use provided base_course or fall back to original
+ question_base_course = (
+ new_base_course
+ if new_base_course is not None
+ else original_question.base_course
+ )
+
# Create new question with copied data
new_question = Question(
- base_course=original_question.base_course,
+ base_course=question_base_course,
name=new_name,
chapter=original_question.chapter,
subchapter=original_question.subchapter,
- author=original_question.author,
+ author=new_owner,
question=original_question.question,
timestamp=canonical_utcnow(),
question_type=original_question.question_type,
@@ -802,9 +811,9 @@ async def copy_question(
await session.flush()
await session.refresh(new_question)
- # If assignment_id is provided, also copy the assignment question
+ # If assignment_id is provided, also add the new question to the assignment
if assignment_id:
- # Get the original assignment question
+ # Try to get the original assignment question for copying settings
original_aq_query = select(AssignmentQuestion).where(
(AssignmentQuestion.question_id == original_question_id)
& (AssignmentQuestion.assignment_id == assignment_id)
@@ -812,14 +821,15 @@ async def copy_question(
original_aq_result = await session.execute(original_aq_query)
original_aq = original_aq_result.scalars().first()
- if original_aq:
- # Get the next sorting priority
- max_priority_query = select(
- func.max(AssignmentQuestion.sorting_priority)
- ).where(AssignmentQuestion.assignment_id == assignment_id)
- max_priority_result = await session.execute(max_priority_query)
- max_priority = max_priority_result.scalar() or 0
+ # Get the next sorting priority
+ max_priority_query = select(
+ func.max(AssignmentQuestion.sorting_priority)
+ ).where(AssignmentQuestion.assignment_id == assignment_id)
+ max_priority_result = await session.execute(max_priority_query)
+ max_priority = max_priority_result.scalar() or 0
+ if original_aq:
+ # Copy settings from the original assignment question
new_assignment_question = AssignmentQuestion(
assignment_id=assignment_id,
question_id=new_question.id,
@@ -831,7 +841,20 @@ async def copy_question(
sorting_priority=max_priority + 1,
activities_required=original_aq.activities_required,
)
- session.add(new_assignment_question)
+ else:
+ # Original question wasn't in this assignment; add with defaults
+ new_assignment_question = AssignmentQuestion(
+ assignment_id=assignment_id,
+ question_id=new_question.id,
+ points=1,
+ timed=False,
+ autograde="pct_correct",
+ which_to_grade="best_answer",
+ reading_assignment=False,
+ sorting_priority=max_priority + 1,
+ activities_required=0,
+ )
+ session.add(new_assignment_question)
await session.commit()
return QuestionValidator.from_orm(new_question)