diff --git a/fuel/app/classes/materia/api/v1.php b/fuel/app/classes/materia/api/v1.php index b173b7be3..ea536f8a2 100644 --- a/fuel/app/classes/materia/api/v1.php +++ b/fuel/app/classes/materia/api/v1.php @@ -237,6 +237,10 @@ static public function widget_instance_update($inst_id=null, $name=null, $qset=n // student made widgets are locked forever if ($inst->is_student_made) { + if ($guest_access === false) + { + return new Msg('Student-made widgets must stay in guest access mode.', 'Student Made', 'error', false); + } $attempts = -1; $guest_access = true; } @@ -346,7 +350,7 @@ static public function widget_instance_update($inst_id=null, $name=null, $qset=n $access = Perm_Manager::get_all_users_explicit_perms($inst_id, Perm::INSTANCE)['widget_user_perms']; foreach ($access as $user_id => $user_perms) { - if (Perm_Manager::is_student($user_id)) + if (Perm_Manager::is_student($user_id) && $user_id != $inst->user_id) { \Model_Notification::send_item_notification(\Model_user::find_current_id(), $user_id, Perm::INSTANCE, $inst_id, 'disabled', null); Perm_Manager::clear_user_object_perms($inst_id, Perm::INSTANCE, $user_id); diff --git a/fuel/app/classes/materia/perm/manager.php b/fuel/app/classes/materia/perm/manager.php index bf3e703da..043f9aae1 100644 --- a/fuel/app/classes/materia/perm/manager.php +++ b/fuel/app/classes/materia/perm/manager.php @@ -38,7 +38,7 @@ static public function is_super_user() // The session caching has been removed due to issues related to the cache when the role is added or revoked // Ideally we can still find a way to cache this and make it more performant!! return (\Fuel::$is_cli === true && ! \Fuel::$is_test) || self::does_user_have_role([\Materia\Perm_Role::SU]); - + } /** @@ -351,10 +351,10 @@ static public function remove_users_from_roles_system_only(Array $user_ids = [], ->execute(); } } + return $success; } - /* ********************** User to Object Rights *************************************** */ diff --git a/src/components/attempts-slider.jsx b/src/components/attempts-slider.jsx index 1fb7a0e37..d862035b5 100644 --- a/src/components/attempts-slider.jsx +++ b/src/components/attempts-slider.jsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react' import './my-widgets-settings-dialog.scss' -const AttemptsSlider = ({inst, parentState, setParentState}) => { +const AttemptsSlider = ({inst, is_student, parentState, setParentState, currentAttemptsVal}) => { const [rawSliderVal, setRawSliderVal] = useState(parseInt(parentState.sliderVal)) const [sliderStopped, setSliderStopped] = useState(false) @@ -16,12 +16,19 @@ const AttemptsSlider = ({inst, parentState, setParentState}) => { const sliderStop = e => { setSliderStopped(true) } - + // now that the slider value isn't actively changing, round the raw value to the nearest stop // pass that rounded value up to the parent component useEffect(() => { if (sliderStopped && parentState.formData.changes.access != 'guest') { const sliderInfo = getSliderInfo(rawSliderVal) + // students cannot change attempts to anything other than + // the original number of attempts or unlimited + if (is_student && sliderInfo.val != currentAttemptsVal && sliderInfo.val != '100') { + setSliderStopped(false) + setRawSliderVal(parseInt(parentState.sliderVal)) + return + } setParentState({...parentState, sliderVal: sliderInfo.val, lastActive: sliderInfo.last}) setSliderStopped(false) } @@ -66,16 +73,16 @@ const AttemptsSlider = ({inst, parentState, setParentState}) => { const updateSliderNum = (val, index) => { // Attempts always unlimited when guest access is true if (parentState.formData.changes.access === 'guest') return + if (is_student && val != currentAttemptsVal && val != '100') return setParentState({...parentState, sliderVal: val.toString(), lastActive: index}) } const generateStopSpan = (stopId, sliderPosition, display) => { - const spanClass = parentState.lastActive === stopId ? 'active' : '' const stopClickHandler = () => updateSliderNum(sliderPosition, stopId) return ( {display} diff --git a/src/components/hooks/useSupportUpdateWidget.jsx b/src/components/hooks/useSupportUpdateWidget.jsx index 5359721d3..a4f20e044 100644 --- a/src/components/hooks/useSupportUpdateWidget.jsx +++ b/src/components/hooks/useSupportUpdateWidget.jsx @@ -9,6 +9,10 @@ export default function useSupportUpdateWidget() { apiUpdateWidget, { onSuccess: (data, variables) => { + if (!data || (data.type == 'error')) + { + return variables.errorFunc(data.msg) + } variables.successFunc() // Refresh widgets @@ -20,7 +24,7 @@ export default function useSupportUpdateWidget() { }, onError: (err, newWidget, context) => { queryClient.setQueryData('widgets', context.previousValue) - + variables.errorFunc() } } diff --git a/src/components/hooks/useUpdateWidget.jsx b/src/components/hooks/useUpdateWidget.jsx index 39321e331..14ad14656 100644 --- a/src/components/hooks/useUpdateWidget.jsx +++ b/src/components/hooks/useUpdateWidget.jsx @@ -19,7 +19,6 @@ export default function useUpdateWidget() { return { widgetList } }, onSuccess: (updatedInst, variables) => { - // update successful - insert new values into our local copy of widgetList for (const page of widgetList?.pages) { for (const inst of page?.pagination) { @@ -33,7 +32,7 @@ export default function useUpdateWidget() { } } } - + // update query cache for widgets. This does NOT invalidate the cache, forcing a re-fetch!! queryClient.setQueryData('widgets', widgetList) diff --git a/src/components/my-widgets-collaborate-dialog.jsx b/src/components/my-widgets-collaborate-dialog.jsx index ea0c0d28d..909eca400 100644 --- a/src/components/my-widgets-collaborate-dialog.jsx +++ b/src/components/my-widgets-collaborate-dialog.jsx @@ -262,6 +262,12 @@ const MyWidgetsCollaborateDialog = ({onClose, inst, myPerms, otherUserPerms, set return
} + if (user.id == inst.user_id) { + user.is_owner = true; + } else { + user.is_owner = false; + } + return - + {`${user.first} ${user.last}`} diff --git a/src/components/my-widgets-scores.jsx b/src/components/my-widgets-scores.jsx index c13e6429b..50d56119d 100644 --- a/src/components/my-widgets-scores.jsx +++ b/src/components/my-widgets-scores.jsx @@ -7,7 +7,7 @@ import LoadingIcon from './loading-icon' import NoScoreContent from'./no-score-content' import './my-widgets-scores.scss' -const MyWidgetsScores = ({inst, beardMode}) => { +const MyWidgetsScores = ({inst, beardMode, is_student}) => { const [state, setState] = useState({ isShowingAll: false, hasScores: false, @@ -16,7 +16,7 @@ const MyWidgetsScores = ({inst, beardMode}) => { const { data: currScores, isFetched } = useQuery({ queryKey: ['score-summary', inst.id], queryFn: () => apiGetScoreSummary(inst.id), - enabled: !!inst && !!inst.id, + enabled: !!inst && !!inst.id && !is_student, staleTime: Infinity, placeholderData: [] }) @@ -61,7 +61,9 @@ const MyWidgetsScores = ({inst, beardMode}) => { const handleShowOlderClick = () => setState({...state, isShowingAll: !state.isShowingAll}) let contentRender = - if (isFetched) { + if (is_student) { + contentRender =

Students cannot view scores.

+ } else if (isFetched) { contentRender = if (state.hasScores || containsStorage()) { const semesterElements = displayedSemesters.map(semester => ( @@ -99,8 +101,8 @@ const MyWidgetsScores = ({inst, beardMode}) => {

Student Activity

+ className={`aux_button ${inst.is_draft ? 'disabled' : ''}`} + onClick={openExport}> Export Options diff --git a/src/components/my-widgets-scores.scss b/src/components/my-widgets-scores.scss index 1dd65f970..2e30b0ee9 100644 --- a/src/components/my-widgets-scores.scss +++ b/src/components/my-widgets-scores.scss @@ -10,6 +10,12 @@ overflow-y: auto; padding-bottom: 1em; + &.limited_because_student { + filter: blur(3px); + user-select: none; + pointer-events: none; + } + #export_scores_button.disabled, #export_scores_button.disabled:hover { background: #d4d4d4; diff --git a/src/components/my-widgets-selected-instance.jsx b/src/components/my-widgets-selected-instance.jsx index b4d8b96f1..df74306ae 100644 --- a/src/components/my-widgets-selected-instance.jsx +++ b/src/components/my-widgets-selected-instance.jsx @@ -499,7 +499,7 @@ const MyWidgetSelectedInstance = ({ { warningDialogRender } { settingsDialogRender } { lockedDialogRender } - + ) } diff --git a/src/components/my-widgets-settings-dialog.jsx b/src/components/my-widgets-settings-dialog.jsx index db5fe6733..e295b629b 100644 --- a/src/components/my-widgets-settings-dialog.jsx +++ b/src/components/my-widgets-settings-dialog.jsx @@ -95,6 +95,7 @@ const MyWidgetsSettingsDialog = ({ onClose, inst, currentUser, otherUserPerms, o } } }) + inst.is_embedded = true // Used for initialization useEffect(() => { @@ -225,7 +226,7 @@ const MyWidgetsSettingsDialog = ({ onClose, inst, currentUser, otherUserPerms, o const changes = state.formData.changes const openClose = validateFormData(changes.dates, changes.times, changes.periods) const errInfo = getErrorInfo(openClose[2]) // Creates an error message if needed - const errMsg = errInfo.msg + let errMsg = errInfo.msg const errors = errInfo.errors let form = { inst_id: inst.id, @@ -244,6 +245,10 @@ const MyWidgetsSettingsDialog = ({ onClose, inst, currentUser, otherUserPerms, o form.attempts = -1 } + if (currentUser.is_student && form.attempts != inst.attempts && form.attempts != -1 ) { + errMsg = "Cannot set attempts to " + form.attempts + ". Students can keep the current number of attempts or set the attempt limits to Unlimited." + } + // Submits the form if there are no errors if (errMsg.length === 0) { let args = [ @@ -431,13 +436,20 @@ const MyWidgetsSettingsDialog = ({ onClose, inst, currentUser, otherUserPerms, o } let studentLimitWarningRender = null - if ( currentUser.is_student) { + if ( currentUser.is_student && currentUser.id != inst.user_id ) { studentLimitWarningRender = (

You are viewing a limited version of this page due to your current role as a student. Students do not have permission to change certain settings like attempt limits or access levels.

) + } else if (currentUser.is_student && currentUser.id == inst.user_id) { + studentLimitWarningRender = ( +

+ You are viewing a limited version of this page due to your current role as a student. + Owners who are students may only change access to Guest Mode and increase attempt limits to Unlimited. +

+ ) } const handlePeriodSelectFormDataChange = data => setState({...state, formData: data}) @@ -472,6 +484,16 @@ const MyWidgetsSettingsDialog = ({ onClose, inst, currentUser, otherUserPerms, o ) } + // Viewing access settings + let canViewNormal = !inst.is_student_made + let canViewEmbedded = inst.is_embedded && !inst.is_student_made + let canViewGuest = !currentUser.is_student || currentUser.id == inst.user_id + || inst.is_student_made + // Editing access settings + let canEditNormal = canViewNormal && !currentUser.is_student + let canEditGuest = !currentUser.is_student || currentUser.id == inst.user_id + let canEditEmbedded = canViewEmbedded && !currentUser.is_student + return (
@@ -481,21 +503,21 @@ const MyWidgetsSettingsDialog = ({ onClose, inst, currentUser, otherUserPerms, o
{ studentLimitWarningRender }
    -
  • +
  • Attempts

    - +
    • { periodSelectElements }
    • Access

      -
        - {currentUser.is_student && !inst.is_student_made ?
      • Access settings are currently disabled because of your student status.
      • : ''} -
      • +
          + {currentUser.is_student && !inst.is_student_made ?
        • Access settings are currently limited because of your student status.
        • : ''} +
        • accessChange('normal')} /> @@ -505,11 +527,11 @@ const MyWidgetsSettingsDialog = ({ onClose, inst, currentUser, otherUserPerms, o The widget can be distributed via URL, embed code, or as an assignment in your LMS.
-
  • +
  • accessChange('guest')} /> @@ -523,14 +545,15 @@ const MyWidgetsSettingsDialog = ({ onClose, inst, currentUser, otherUserPerms, o
  • + className={`embed-only ${canViewEmbedded ? ' show' : ''} ${!canEditEmbedded ? ' limited-because-student disabled' : ''}`} aria-hidden={!canViewEmbedded}> {accessChange('embed')}} /> - +
    This widget will not be playable outside of the classes it is embedded within. diff --git a/src/components/my-widgets-settings-dialog.scss b/src/components/my-widgets-settings-dialog.scss index 419709eef..3e2241d9d 100644 --- a/src/components/my-widgets-settings-dialog.scss +++ b/src/components/my-widgets-settings-dialog.scss @@ -2,7 +2,6 @@ display: flex; flex-direction: column; width: 680px; - max-height: 675px; padding: 10px 10px 0 10px; .top-bar { @@ -199,6 +198,10 @@ &.active { color: rgba(0, 0, 0, 1); } + &.disabled { + color: rgba(0,0,0,0.5); + cursor: default; + } } // Entire bar is 67% so 2.68% per 1 attempt @@ -276,15 +279,13 @@ } } - &.limited-because-student { + .limited-because-student { - li.normal.show, li.guest-mode { - filter: blur(3px); - user-select: none; - pointer-events: none; - } + filter: blur(3px); + user-select: none; + pointer-events: none; - li.studentWarningListItem { + &.studentWarningListItem { margin: 0 0 15px 0; } } @@ -301,7 +302,7 @@ font-size: 13px; } - .normal { + .normal, .embed-only { display: none; &.show { @@ -317,17 +318,14 @@ margin-left: 5px; } - .guest-mode { - &.disabled { - label { - color: #b5b3b3; - } + .disabled { + label { + color: #b5b3b3; } } } #embedded-only { - display: none; &.show { display: block; diff --git a/src/components/support-selected-instance.jsx b/src/components/support-selected-instance.jsx index ae062a837..4d2e6ea9b 100644 --- a/src/components/support-selected-instance.jsx +++ b/src/components/support-selected-instance.jsx @@ -189,8 +189,8 @@ const SupportSelectedInstance = ({inst, currentUser, embed = false}) => { setSuccessText('Success!') setErrorText('') }, - errorFunc: () => { - setErrorText('Error: Update Unsuccessful') + errorFunc: (msg) => { + setErrorText('Error: ' + msg) setSuccessText('') } }) diff --git a/src/components/widget-admin-list-card.jsx b/src/components/widget-admin-list-card.jsx index ac3c8dd34..a1a9dfc1c 100644 --- a/src/components/widget-admin-list-card.jsx +++ b/src/components/widget-admin-list-card.jsx @@ -37,7 +37,7 @@ const WidgetListCard = ({widget = null}) => { event.persist() setState(prevState => ({...prevState, widget: {...prevState.widget, meta_data: {...prevState.widget.meta_data, demo: event.target.value}}})) } - + const handleAboutChange = event => { event.persist() setState(prevState => ({...prevState, widget: {...prevState.widget, meta_data: {...prevState.widget.meta_data, about: event.target.value}}})) @@ -81,7 +81,7 @@ const WidgetListCard = ({widget = null}) => { excerpt: state.widget.meta_data.excerpt, demo: state.widget.meta_data.demo, } - + apiUpdateWidgetAdmin(update).then(response => { let errorMessage = [] let success = false @@ -136,7 +136,7 @@ const WidgetListCard = ({widget = null}) => { {state.widget.name}
    - { ! state.widget.expanded ? <> : + { ! state.widget.expanded ? <> :
    { widgetErrorsRender } { widgetSuccessRender } @@ -201,7 +201,7 @@ const WidgetListCard = ({widget = null}) => {
    -
    diff --git a/src/util/api.js b/src/util/api.js index 1008d1b4d..6740f5f8d 100644 --- a/src/util/api.js +++ b/src/util/api.js @@ -361,7 +361,10 @@ export const apiCanEditWidgets = arrayOfWidgetIds => { */ export const apiUpdateWidget = ({ args }) => { return fetch('/api/json/widget_instance_update', fetchOptions({ body: `data=${formatFetchBody(args)}` })) - .then(res => res.json()) + .then(resp => { + if (resp.status === 204 || resp.status === 502) return [] + return resp.json() + }) .then(widget => widget) }