Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { assignmentSelectors } from "@store/assignment/assignment.logic";
import { useCreateNewExerciseMutation } from "@store/exercises/exercises.logic.api";
import { toast } from "react-hot-toast";
import { useSelector } from "react-redux";

import { CreateExerciseFormType } from "@/types/exercises";
import { buildQuestionJson } from "@/utils/questionJson";
Expand All @@ -26,6 +28,7 @@ export const CreateView = ({
setIsSaving
}: CreateViewProps) => {
const [createNewExercise] = useCreateNewExerciseMutation();
const assignmentId = useSelector(assignmentSelectors.getSelectedAssignmentId);

return (
<CreateExercise
Expand All @@ -49,7 +52,9 @@ export const CreateView = ({
source: "This question was written in the web interface",
tags: data.tags,
topic: data.topic,
points: data.points
points: data.points,
is_reading: false,
assignment_id: assignmentId!
});

if (response.data) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Chips } from "primereact/chips";
import { Dropdown } from "primereact/dropdown";
import { InputNumber } from "primereact/inputnumber";
import { InputNumber, InputNumberChangeEvent } from "primereact/inputnumber";
import { InputText } from "primereact/inputtext";
import { ReactNode, useCallback, useEffect, useState } from "react";

Expand Down Expand Up @@ -77,6 +77,14 @@ export const BaseExerciseSettingsContent = <T extends BaseExerciseSettings>({
const chapterError = !settings.chapter;
const pointsError = settings.points <= 0;

const defaultValues = {
difficulty: 1,
points: 3
};
const onChangeInputNumber = (e: InputNumberChangeEvent, field: "difficulty" | "points") => {
updateSetting(field, e.value !== null ? e.value : defaultValues[field]);
};

return (
<>
<div className={styles.settingsGrid}>
Expand Down Expand Up @@ -144,7 +152,7 @@ export const BaseExerciseSettingsContent = <T extends BaseExerciseSettings>({
min={0}
max={100000}
className={`w-full ${pointsError ? styles.requiredField : ""}`}
onValueChange={(e) => updateSetting("points", e.value !== null ? e.value : 1)}
onChange={(e) => onChangeInputNumber(e, "points")}
/>
<label htmlFor="points">Points*</label>
</span>
Expand All @@ -161,7 +169,7 @@ export const BaseExerciseSettingsContent = <T extends BaseExerciseSettings>({
min={1}
max={5}
className="w-full"
onValueChange={(e) => updateSetting("difficulty", e.value !== null ? e.value : 3)}
onChange={(e) => onChangeInputNumber(e, "difficulty")}
/>
<label htmlFor="difficulty">Difficulty</label>
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { assignmentExerciseApi } from "@store/assignmentExercise/assignmentExerc
import { baseQuery } from "@store/baseQuery";
import toast from "react-hot-toast";

import { RootState } from "@/state/store";
import { DetailResponse } from "@/types/api";
import {
CreateExercisesPayload,
Expand All @@ -20,24 +19,13 @@ export const exercisesApi = createApi({
createNewExercise: build.mutation<number, CreateExercisesPayload>({
query: (body) => ({
method: "POST",
url: "/assignment/instructor/new_question",
url: "/assignment/instructor/question",
body
}),
transformResponse: (response: DetailResponse<{ id: number }>) => {
return response.detail.id;
},
onQueryStarted: (_, { queryFulfilled, dispatch, getState }) => {
onQueryStarted: (_, { queryFulfilled, dispatch }) => {
queryFulfilled
.then((response) => {
const state = getState() as RootState;

dispatch(
assignmentExerciseApi.endpoints.updateAssignmentExercises.initiate({
idsToAdd: [response.data],
isReading: false,
assignmentId: state.assignmentTemp.selectedAssignmentId!
})
);
.then(() => {
dispatch(assignmentExerciseApi.util.invalidateTags(["Exercises"]));
})
.catch(() => {
toast("Error creating new exercise", {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,7 @@
padding-bottom: 0;
}
}

.p-datatable-wrapper {
overflow: visible !important;
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ export type CreateExercisesPayload = {
tags: string;
topic: string;
points: number;
is_reading: boolean;
assignment_id: number;
};

export const isExerciseType = (value: any): value is ExerciseType => {
Expand Down
51 changes: 50 additions & 1 deletion bases/rsptx/assignment_server_api/routers/instructor.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,9 @@
fetch_one_assignment,
get_peer_votes,
search_exercises,
create_api_token,
create_api_token
)
from rsptx.db.crud.assignment import add_assignment_question
from rsptx.auth.session import auth_manager, is_instructor
from rsptx.templates import template_folder
from rsptx.configuration import settings
Expand All @@ -85,6 +86,7 @@
UpdateAssignmentExercisesPayload,
AssignmentQuestionUpdateDict,
ExercisesSearchRequest,
CreateExercisesPayload
)
from rsptx.logging import rslogger
from rsptx.analytics import log_this_function
Expand Down Expand Up @@ -1254,6 +1256,53 @@ async def do_assignment_summary_data(
},
)

@router.post("/question")
@instructor_role_required()
async def question_creation(
request_data: CreateExercisesPayload,
request: Request,
user=Depends(auth_manager)
):

if not request_data.author:
request_data.author = user.first_name + " " + user.last_name

course = await fetch_course(user.course_name)

# Exclude assignment_id and is_reading parameters
question_data = {key: value for key, value in request_data.model_dump().items() if key not in ("assignment_id", "is_reading")}

try:
question = await create_question(
QuestionValidator(
**question_data,
base_course=course.base_course,
subchapter="Exercises",
timestamp=canonical_utcnow(),
is_private=False,
practice=False,
from_source=False,
review_flag=False,
owner=user.username
)
)

await add_assignment_question(
data=request_data,
question=question
)

except Exception as e:
rslogger.error(f"Error creating question: {e}")
return make_json_response(
status=status.HTTP_400_BAD_REQUEST,
detail=f"Error creating question: {e}"
)

return make_json_response(
status=status.HTTP_201_CREATED
)


class AddTokenRequest(BaseModel):
provider: str
Expand Down
49 changes: 48 additions & 1 deletion components/rsptx/db/crud/assignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from asyncpg.exceptions import UniqueViolationError
from rsptx.validation import schemas
from rsptx.validation.schemas import (
AssignmentQuestionUpdateDict,
AssignmentQuestionUpdateDict, CreateExercisesPayload,
)
from rsptx.data_types.which_to_grade import WhichToGradeOptions
from rsptx.data_types.autograde import AutogradeOptions
Expand Down Expand Up @@ -500,6 +500,53 @@ async def update_assignment_exercises(
"total_points": assignment.points,
}

async def add_assignment_question(
data: CreateExercisesPayload,
question: Question
) -> None:
print(type(question))
async with async_session() as session:
assignment_result = await session.execute(
select(Assignment).where(
Assignment.id == data.assignment_id
)
)
assignment = assignment_result.scalar_one_or_none()

if not assignment:
raise Exception(f"The assignment with id {data.assignment_id} is not found.")

# Get the maximum sorting_priority considering isReading
query_max_priority = select(
func.max(AssignmentQuestion.sorting_priority)
).where(
AssignmentQuestion.assignment_id == data.assignment_id,
AssignmentQuestion.reading_assignment == data.is_reading,
)
max_priority_result = await session.execute(query_max_priority)
max_sort_priority = (
max_priority_result.scalar() or 0
) # If there are no records, start from 0

session.add(
AssignmentQuestion(
assignment_id=data.assignment_id,
question_id=question.id,
points=data.points, # Use the points from the question
timed=None, # Leave as null
autograde=(
"interaction" if data.is_reading else "pct_correct"
), # Depends on isReading
which_to_grade="best_answer",
reading_assignment=data.is_reading,
sorting_priority=max_sort_priority,
activities_required=None
)
)
assignment.points += data.points

await session.commit()


async def reorder_assignment_questions(question_ids: List[int]):
"""
Expand Down
19 changes: 19 additions & 0 deletions components/rsptx/validation/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,3 +347,22 @@ class AssignmentQuestionUpdateDict(TypedDict, total=False):

# Owner field for permission checking
owner: Optional[str]

class CreateExercisesPayload(BaseModel):
id: Optional[int] = None
name: str
source: str
question_type: str
htmlsrc: str
autograde: Optional[str] = None
question_json: Json
chapter: Optional[str] = None
author: Optional[str] = None
tags: Optional[str] = None
description: Optional[str] = None
difficulty: Optional[float] = None
topic: Optional[str] = None
points: Optional[int] = None

is_reading: bool
assignment_id: int