diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/renderers/assignment.jsx b/bases/rsptx/assignment_server_api/assignment_builder/src/renderers/assignment.jsx index f757bb77..8146fa2b 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/renderers/assignment.jsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/renderers/assignment.jsx @@ -53,6 +53,9 @@ import { setReleased, setTimeLimit, setVisible, + setHidden, + selectVisible, + selectHidden, } from "../state/assignment/assignSlice"; import { EditorContainer, EditorChooser } from "./editorModeChooser.jsx"; @@ -79,7 +82,7 @@ function handleChange() { if (currentValue.id !== 0 && previousValue && previousValue.id !== 0) { let changes = diff(previousValue, currentValue) let keys = Object.keys(changes) - let updateKeys = ["due", "points", "visible", "time_limit", "peer_async_visible", "is_peer", "is_timed", "nopause", "nofeedback", "description"] + let updateKeys = ["due","vis", "hid", "points", "visible", "time_limit", "peer_async_visible", "is_peer", "is_timed", "nopause", "nofeedback", "description"] let update = keys.filter((k) => updateKeys.includes(k)) if (update.length > 0 && keys.indexOf("id") === -1) { console.log(`updating assignment ${update}`) @@ -114,6 +117,8 @@ function AssignmentEditor() { const name = useSelector(selectName); const desc = useSelector(selectDesc); const due = useSelector(selectDue); + const vis = useSelector(selectVisible) + const hid = useSelector(selectHidden) const points = useSelector(selectPoints); const assignData = useSelector(selectAll); const [items, setItems] = useState(assignData.all_assignments.map((a) => a.name)) @@ -143,7 +148,8 @@ function AssignmentEditor() { dispatch(setDue(current.duedate)); dispatch(setPoints(current.points)); dispatch(setId(current.id)); - dispatch(setVisible(current.visible)); + dispatch(setVisible(current.visibledate)); + dispatch(setHidden(current.hindingdate)); dispatch(setIsPeer(current.is_peer)); dispatch(setIsTimed(current.is_timed)); dispatch(setFromSource(current.from_source)); @@ -167,15 +173,24 @@ function AssignmentEditor() { name: name, description: desc, duedate: due, + visibledate: vis, + hiddingdate: hid, points: points, } dispatch(createAssignment(assignment)) // reset items so create button disappears setItems(assignData.all_assignments.map((a) => a.name)) } + let placeDate = new Date(due); const [datetime12h, setDatetime12h] = useState(placeDate); + let placeDate1 = new Date(vis); + const [visibleDate, setVisibleDate] = useState(placeDate1); + + let placeDate2 = new Date(hid); + const [hiddenDate, setHiddenDate] = useState(placeDate2); + // We use two representations because the Calendar, internally wants a date // but Redux gets unhappy with a Date object because its not serializable. // So we use a string for Redux and a Date object for the Calendar. @@ -186,79 +201,110 @@ function AssignmentEditor() { dispatch(setDue(d)); } - return ( -
-
-
- - - {items.length == 0 && name ? - - : null} - - dispatch(setDesc(e.target.value))} - /> -
-
- - -
- -
- - dispatch(setPoints(e.value))} - /> -
- - dispatch(setVisible(e.value))} /> -
-
-
-
+ const handleVisibleChange = (e) => { + setVisibleDate(e.value); + console.log(`visible change ${visibleDate} ${e.value}`) + let d = e.value.toISOString().replace("Z", ""); + dispatch(setVisible(d)); + }; + + const handleHiddenChange = (e) => { + console.log(`hidden change ${hiddenDate} ${e.value}`) + let d = e.value.toISOString().replace("Z", ""); + dispatch(setHidden(d)); + }; + + return ( +
+
+
+ + + {items.length === 0 && name ? ( + + ) : null} + + dispatch(setDesc(e.target.value))} + /> +
+
+ + +
+
+ + dispatch(setPoints(e.value))} + /> +
+
+ +
+ +
+ + +
+ +
- ); +
+
+ ); } diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/state/assignment/assignSlice.js b/bases/rsptx/assignment_server_api/assignment_builder/src/state/assignment/assignSlice.js index 4c45e594..43484769 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/state/assignment/assignSlice.js +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/state/assignment/assignSlice.js @@ -82,10 +82,14 @@ export const createAssignment = createAsyncThunk( // make a date that is acceptable on the server let duedate = new Date(assignData.duedate); duedate = duedate.toISOString().replace('Z', ''); + let visibledate = new Date(assignData.visibledate).toISOString().replace('Z', ''); + let hiddingdate = new Date(assignData.hiddingdate).toISOString().replace('Z', ''); let body = { name: assignData.name, description: assignData.description, duedate: duedate, + visibledate: visibledate, + hiddingdate: hiddingdate, points: assignData.points, kind: "quickcode", }; @@ -303,7 +307,10 @@ let epoch = cDate.getTime(); epoch = epoch + 60 * 60 * 24 * 7 * 1000; cDate = new Date(epoch); let defaultDeadline = cDate.toLocaleString(); -// old +let visibleEpoch = epoch - 60 * 60 * 24 * 7 * 1000; +let hiddenEpoch = epoch + 60 * 60 * 24 * 7 * 1000; +let visibleDateDefault = new Date(visibleEpoch).toLocaleString(); +let hiddingDateDefault = new Date(hiddenEpoch).toLocaleString(); // create a slice for Assignments // This slice must be registered with the store in store.js @@ -319,6 +326,7 @@ let defaultDeadline = cDate.toLocaleString(); * - setId * - setPoints * - setVisible + * - setHidden * - setIsPeer * - setIsTimed * - setNoFeedback @@ -371,6 +379,7 @@ let defaultDeadline = cDate.toLocaleString(); * @property {Function} reducers.setDesc - Sets the description of the assignment. * @property {Function} reducers.setDue - Sets the due date of the assignment. * @property {Function} reducers.setVisible - Sets the visibility of the assignment. + * @property {Function} reducers.setHidden - Sets the visibility of the assignment. * @property {Function} reducers.setIsPeer - Sets if the assignment is a peer assignment. * @property {Function} reducers.setIsTimed - Sets if the assignment is timed. * @property {Function} reducers.setNoFeedback - Sets if the assignment has no feedback. @@ -398,7 +407,8 @@ export const assignSlice = createSlice({ desc: "", duedate: defaultDeadline, points: 1, - visible: true, + visibledate: visibleDateDefault, + hiddingdate: hiddingDateDefault, is_peer: false, is_timed: false, nofeedback: true, @@ -437,8 +447,25 @@ export const assignSlice = createSlice({ state.duedate = action.payload.toISOString().replace('Z', '') }, setVisible: (state, action) => { - state.visible = action.payload; + // action.payload is a Date object coming from the date picker or a string from the server + // convert it to a string and remove the Z because we don't expect timezone information + if (typeof action.payload === "string") { + state.visibledate = action.payload; + return; + } + state.visibledate = action.payload.toISOString().replace('Z', '') + }, + + setHidden: (state, action) => { + // action.payload is a Date object coming from the date picker or a string from the server + // convert it to a string and remove the Z because we don't expect timezone information + if (typeof action.payload === "string") { + state.hiddingdate = action.payload; + return; + } + state.hiddingdate = action.payload.toISOString().replace('Z', '') }, + setIsPeer: (state, action) => { state.is_peer = action.payload; }, @@ -596,6 +623,7 @@ export const { setReleased, setTimeLimit, setVisible, + setHidden, sumPoints, updateExercise, updateField, @@ -627,5 +655,6 @@ export const selectPoints = (state) => state.assignment.points; export const selectQuestionCount = (state) => state.assignment.question_count; export const selectSearchResults = (state) => state.assignment.search_results; export const selectTimeLimit = (state) => state.assignment.time_limit; -export const selectVisible = (state) => state.assignment.visible; +export const selectVisible = (state) => state.assignment.visibledate; +export const selectHidden = (state) => state.assignment.hiddingdate; export default assignSlice.reducer; diff --git a/bases/rsptx/assignment_server_api/routers/instructor.py b/bases/rsptx/assignment_server_api/routers/instructor.py index fbfaef04..f1418319 100644 --- a/bases/rsptx/assignment_server_api/routers/instructor.py +++ b/bases/rsptx/assignment_server_api/routers/instructor.py @@ -169,6 +169,7 @@ async def new_assignment( **request_data.model_dump(), course=course.id, visible=True, + hidden=False, released=False, from_source=False, is_peer=False, diff --git a/bases/rsptx/assignment_server_api/routers/student.py b/bases/rsptx/assignment_server_api/routers/student.py index 6e3d33a7..5a07aa84 100644 --- a/bases/rsptx/assignment_server_api/routers/student.py +++ b/bases/rsptx/assignment_server_api/routers/student.py @@ -84,7 +84,7 @@ async def get_assignments( assignments = await fetch_assignments(course.course_name, is_visible=True) assignments.sort(key=lambda x: x.duedate, reverse=True) stats_list = await fetch_all_assignment_stats(course.course_name, user.id) - stats = {} + stats = {s.assignment: s for s in stats_list} for s in stats_list: stats[s.assignment] = s rslogger.debug(f"stats: {stats}") @@ -206,11 +206,12 @@ async def doAssignment( return RedirectResponse("/assignment/student/chooseAssignment") - if ( - assignment.visible == "F" - or assignment.visible is None - or assignment.visible == False - ): + current_time = datetime.datetime.utcnow() + + if not (assignment.visibledate <= current_time and + + (assignment.hideDate is None or assignment.hideDate >= current_time)): + if await is_instructor(request) is False: rslogger.error( f"Attempt to access invisible assignment {assignment_id} by {user.username}" @@ -219,6 +220,7 @@ async def doAssignment( if assignment.points is None: assignment.points = 0 + # This query assumes that questions are on a page and in a subchapter that is # present in the book. For many questions that is of course a given. But for diff --git a/components/rsptx/db/crud.py b/components/rsptx/db/crud.py index 3daa90d5..fa57628a 100644 --- a/components/rsptx/db/crud.py +++ b/components/rsptx/db/crud.py @@ -1260,6 +1260,7 @@ async def fetch_assignments( course_name: str, is_peer: Optional[bool] = False, is_visible: Optional[bool] = False, + is_hidden: Optional[bool] = True, fetch_all: Optional[bool] = False, ) -> List[AssignmentValidator]: """ @@ -1271,10 +1272,13 @@ async def fetch_assignments( :param is_peer: bool, whether or not the assignment is a peer assignment :return: List[AssignmentValidator], a list of AssignmentValidator objects """ + now = datetime.utcnow() + # Visibility clause if is_visible: - vclause = Assignment.visible == is_visible - else: + vclause = or_(Assignment.visible == is_visible, and_(Assignment.visibledate >= now, Assignment.hiddingdate <= now) + ) + else: vclause = True if is_peer: @@ -1301,13 +1305,12 @@ async def fetch_assignments( .order_by(Assignment.duedate.desc()) ) + async with async_session() as session: res = await session.execute(query) - rslogger.debug(f"{res=}") return [AssignmentValidator.from_orm(a) for a in res.scalars()] - -# write a function that fetches all Assignment objects given a course name +# write a function that fetches one Assignment objects given a course name async def fetch_one_assignment(assignment_id: int) -> AssignmentValidator: """ Fetch one Assignment object @@ -1316,8 +1319,14 @@ async def fetch_one_assignment(assignment_id: int) -> AssignmentValidator: :return: AssignmentValidator """ - - query = select(Assignment).where(Assignment.id == assignment_id) + now = datetime.utcnow() + query = select(Assignment).where( + and_( + Assignment.id == assignment_id, + Assignment.visibledate <= now, + Assignment.hiddingdate >= now + ) + ) async with async_session() as session: res = await session.execute(query) @@ -1601,9 +1610,9 @@ async def fetch_questions_by_search_criteria( if criteria.question_type: where_criteria.append(Question.question_type == criteria.question_type) if criteria.author: - where_criteria.append(Question.author.regexp_match(criteria.author, flags="i")) + where_criteria.append(Question.author.regexp_match(criteria.author, flags="i")) if criteria.base_course: - where_criteria.append(Question.base_course == criteria.base_course) + where_criteria.append(Question.base_course == criteria.base_course) if len(where_criteria) == 0: raise ValueError("No search criteria provided") diff --git a/components/rsptx/db/models.py b/components/rsptx/db/models.py index 03b5de79..3c13f25b 100644 --- a/components/rsptx/db/models.py +++ b/components/rsptx/db/models.py @@ -554,8 +554,11 @@ class Assignment(Base, IdMixin): released = Column(Web2PyBoolean, nullable=False) description = Column(Text) duedate = Column(DateTime, nullable=False) + # visible = Column(Web2PyBoolean, nullable=False) + threshold_pct = Column(Float(53)) visible = Column(Web2PyBoolean, nullable=False) - threshold_pct = Column(Float(53)) + visibledate = Column(DateTime, nullable=False) + hiddingdate = Column(DateTime, nullable=False) allow_self_autograde = Column(Web2PyBoolean) is_timed = Column(Web2PyBoolean) time_limit = Column(Integer) diff --git a/components/rsptx/validation/schemas.py b/components/rsptx/validation/schemas.py index 5abe238c..c52fd7f5 100644 --- a/components/rsptx/validation/schemas.py +++ b/components/rsptx/validation/schemas.py @@ -228,6 +228,8 @@ class AssignmentIncoming(BaseModel): description: str points: int duedate: datetime + visibledate: datetime + hiddingdate: datetime class QuestionIncoming(BaseModel):