From edf31ba8458856f72866f037ecf00ee5cc6f8a1b Mon Sep 17 00:00:00 2001 From: kwizeras Date: Wed, 10 Jul 2024 15:59:18 -0400 Subject: [PATCH 1/8] fixing issue #466 --- .../src/renderers/assignment.jsx | 190 +++++++++++------- .../src/state/assignment/assignSlice.js | 33 ++- .../assignment_server_api/routers/student.py | 11 +- components/rsptx/db/crud.py | 16 +- 4 files changed, 164 insertions(+), 86 deletions(-) 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 5c589973..10f9f767 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 @@ -303,7 +303,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 +322,7 @@ let defaultDeadline = cDate.toLocaleString(); * - setId * - setPoints * - setVisible + * - setHidden * - setIsPeer * - setIsTimed * - setNoFeedback @@ -371,6 +375,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 +403,8 @@ export const assignSlice = createSlice({ desc: "", duedate: defaultDeadline, points: 1, - visible: true, + visibledate: visibleDateDefault, + hiddingdate: hiddingDateDefault, is_peer: false, is_timed: false, nofeedback: true, @@ -436,8 +442,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; }, @@ -592,6 +615,7 @@ export const { setReleased, setTimeLimit, setVisible, + setHidden, sumPoints, updateExercise, updateField, @@ -622,5 +646,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/student.py b/bases/rsptx/assignment_server_api/routers/student.py index 255eb9ce..627278fa 100644 --- a/bases/rsptx/assignment_server_api/routers/student.py +++ b/bases/rsptx/assignment_server_api/routers/student.py @@ -81,7 +81,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}") @@ -202,11 +202,10 @@ 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}") return RedirectResponse("/assignment/student/chooseAssignment") diff --git a/components/rsptx/db/crud.py b/components/rsptx/db/crud.py index d69b9d4f..1ac6a1c9 100644 --- a/components/rsptx/db/crud.py +++ b/components/rsptx/db/crud.py @@ -1267,9 +1267,11 @@ 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 + vclause = and_(Assignment.visibledate <= now, Assignment.hiddingdate >= now) else: vclause = True @@ -1291,7 +1293,7 @@ async def fetch_assignments( 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 @@ -1300,8 +1302,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) From 163989fa194f80f01717d0764d49a81ebc8c71cc Mon Sep 17 00:00:00 2001 From: kwizeras Date: Thu, 11 Jul 2024 14:40:46 -0400 Subject: [PATCH 2/8] added visibiledate and hidingdate columns in the schemas.py --- .../assignment_builder/src/state/assignment/assignSlice.js | 4 ++++ components/rsptx/db/models.py | 6 ++++-- components/rsptx/validation/schemas.py | 2 ++ 3 files changed, 10 insertions(+), 2 deletions(-) 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 c86ceb53..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", }; diff --git a/components/rsptx/db/models.py b/components/rsptx/db/models.py index 03b5de79..bced831a 100644 --- a/components/rsptx/db/models.py +++ b/components/rsptx/db/models.py @@ -554,8 +554,10 @@ 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 a770026c..03ed686c 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): From 4d513e5f9a8677887740bbafd1b0ad985dce8ef7 Mon Sep 17 00:00:00 2001 From: Jalin Jones Date: Thu, 11 Jul 2024 13:50:36 -0500 Subject: [PATCH 3/8] minor test changes --- bases/rsptx/assignment_server_api/routers/instructor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bases/rsptx/assignment_server_api/routers/instructor.py b/bases/rsptx/assignment_server_api/routers/instructor.py index ef371147..2fa0a3ce 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, From d7fc91f53708bad08ffd88dfdd8efff155bf162e Mon Sep 17 00:00:00 2001 From: Jalin Jones Date: Thu, 11 Jul 2024 14:08:46 -0500 Subject: [PATCH 4/8] Testing instructor.py changes --- bases/rsptx/assignment_server_api/routers/instructor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bases/rsptx/assignment_server_api/routers/instructor.py b/bases/rsptx/assignment_server_api/routers/instructor.py index 2fa0a3ce..76106737 100644 --- a/bases/rsptx/assignment_server_api/routers/instructor.py +++ b/bases/rsptx/assignment_server_api/routers/instructor.py @@ -168,8 +168,8 @@ async def new_assignment( new_assignment = AssignmentValidator( **request_data.model_dump(), course=course.id, - visible=True, - hidden=False, + visible=datetime.datetime(), + hidden=datetime.datetime(), released=False, from_source=False, is_peer=False, From 4edb08ef6c0bad3b5b7663658ce68d9369af65f0 Mon Sep 17 00:00:00 2001 From: Jalin Jones Date: Tue, 16 Jul 2024 08:22:26 -0500 Subject: [PATCH 5/8] Changed the hidden and visible objects in the instructor file back into booleans. Fixed a couple of syntax errors in the student file. Created new logic for the crud file for testing when our database is fixed. --- .../routers/instructor.py | 4 +- .../assignment_server_api/routers/student.py | 3 +- components/rsptx/db/crud.py | 40 ++++++++++--------- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/bases/rsptx/assignment_server_api/routers/instructor.py b/bases/rsptx/assignment_server_api/routers/instructor.py index 76106737..2fa0a3ce 100644 --- a/bases/rsptx/assignment_server_api/routers/instructor.py +++ b/bases/rsptx/assignment_server_api/routers/instructor.py @@ -168,8 +168,8 @@ async def new_assignment( new_assignment = AssignmentValidator( **request_data.model_dump(), course=course.id, - visible=datetime.datetime(), - hidden=datetime.datetime(), + 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 936c626d..4a2b4455 100644 --- a/bases/rsptx/assignment_server_api/routers/student.py +++ b/bases/rsptx/assignment_server_api/routers/student.py @@ -207,7 +207,7 @@ async def doAssignment( return RedirectResponse("/assignment/student/chooseAssignment") current_time = datetime.datetime.utcnow() - if not (assignment.visibledate <= current_time and + if not (assignment.visibledate <= current_time and (assignment.hideDate is None or assignment.hideDate >= current_time)): if await is_instructor(request) is False: @@ -218,6 +218,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 59234fe5..f805bab5 100644 --- a/components/rsptx/db/crud.py +++ b/components/rsptx/db/crud.py @@ -1260,6 +1260,8 @@ async def fetch_assignments( course_name: str, is_peer: Optional[bool] = False, is_visible: Optional[bool] = False, + is_hidden: Optional[bool] = True, + ) -> List[AssignmentValidator]: """ Fetch all Assignment objects for the given course name. @@ -1270,32 +1272,34 @@ 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() + now = datetime.utcnow() # Visibility clause + visibility_conditions = [] if is_visible: - vclause = and_(Assignment.visibledate <= now, Assignment.hiddingdate >= now) + visibility_conditions.append(Assignment.visibledate <= now) + if not is_hidden: + visibility_conditions.append(Assignment.hiddingdate >= now) + if visibility_conditions: + vclause = and_(*visibility_conditions) else: - vclause = True + vclause = None # Neutral condition, no visibility filter applied - query = ( - select(Assignment) - .where( - and_( - Assignment.course == Courses.id, - Courses.course_name == course_name, - vclause, - ) - ) - .order_by(Assignment.duedate.desc()) - ) + conditions = [ + Assignment.course == Courses.id, + Courses.course_name == course_name, + ] + if vclause: + conditions.append(vclause) + if is_peer is not None: + conditions.append(Assignment.is_peer == is_peer) + + query = select(Assignment).where(and_(*conditions)).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 one Assignment objects given a course name async def fetch_one_assignment(assignment_id: int) -> AssignmentValidator: """ @@ -1596,9 +1600,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") From 733768b083417669657f3aa22ec30b30e0dd261b Mon Sep 17 00:00:00 2001 From: Jalin Jones Date: Tue, 16 Jul 2024 15:04:19 -0500 Subject: [PATCH 6/8] modified fetch_assignments logic in crud.py file to accomodate new changes from the main repo --- components/rsptx/db/crud.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/components/rsptx/db/crud.py b/components/rsptx/db/crud.py index d51df1b5..ffdc4d3a 100644 --- a/components/rsptx/db/crud.py +++ b/components/rsptx/db/crud.py @@ -1275,15 +1275,9 @@ async def fetch_assignments( now = datetime.utcnow() # Visibility clause - visibility_conditions = [] if is_visible: - visibility_conditions.append(Assignment.visibledate <= now) - if not is_hidden: - visibility_conditions.append(Assignment.hiddingdate >= now) - if visibility_conditions: - vclause = and_(*visibility_conditions) - else: - vclause = None # Neutral condition, no visibility filter applied + vclause = or_(Assignment.visible == is_visible, and_(Assignment.visibledate >= now, Assignment.hiddingdate <= now) + ) if is_peer: pclause = Assignment.is_peer == True # noqa: E712 From 28a9c672d9a54bf03cfa0a4ec4521985b977556f Mon Sep 17 00:00:00 2001 From: Jalin Jones Date: Tue, 16 Jul 2024 15:39:07 -0500 Subject: [PATCH 7/8] modified fetch_assignments logic in crud.py file by adding the vclause else statement back. Mistakenly deleted it. --- components/rsptx/db/crud.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/rsptx/db/crud.py b/components/rsptx/db/crud.py index ffdc4d3a..fa57628a 100644 --- a/components/rsptx/db/crud.py +++ b/components/rsptx/db/crud.py @@ -1278,6 +1278,8 @@ async def fetch_assignments( if is_visible: vclause = or_(Assignment.visible == is_visible, and_(Assignment.visibledate >= now, Assignment.hiddingdate <= now) ) + else: + vclause = True if is_peer: pclause = Assignment.is_peer == True # noqa: E712 From 8d077bb8d3f7ade1d99ead04881081acf900fef7 Mon Sep 17 00:00:00 2001 From: Jalin Jones Date: Tue, 16 Jul 2024 16:02:11 -0500 Subject: [PATCH 8/8] modified models.py by adding the visible boolean column back. Fixed syntax errors in student.py file again. --- bases/rsptx/assignment_server_api/routers/student.py | 2 +- components/rsptx/db/models.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/bases/rsptx/assignment_server_api/routers/student.py b/bases/rsptx/assignment_server_api/routers/student.py index 1cc16685..5a07aa84 100644 --- a/bases/rsptx/assignment_server_api/routers/student.py +++ b/bases/rsptx/assignment_server_api/routers/student.py @@ -208,7 +208,7 @@ async def doAssignment( current_time = datetime.datetime.utcnow() - if not (assignment.visibledate <= current_time and + if not (assignment.visibledate <= current_time and (assignment.hideDate is None or assignment.hideDate >= current_time)): diff --git a/components/rsptx/db/models.py b/components/rsptx/db/models.py index bced831a..3c13f25b 100644 --- a/components/rsptx/db/models.py +++ b/components/rsptx/db/models.py @@ -556,6 +556,7 @@ class Assignment(Base, IdMixin): duedate = Column(DateTime, nullable=False) # visible = Column(Web2PyBoolean, nullable=False) threshold_pct = Column(Float(53)) + visible = Column(Web2PyBoolean, nullable=False) visibledate = Column(DateTime, nullable=False) hiddingdate = Column(DateTime, nullable=False) allow_self_autograde = Column(Web2PyBoolean)