Add Ability For Teacher to Record Student Last Attended Date
fixes COMMS-610 Test Plan * Go To People Tab in Course * Click On Any Student Name * Hopefully see the enrollments page unless context is turned on * If context is turned on then click on the student name * There should be a Last Attended Component after clicking "more user details..." * Add Last Attended Date To Student Change-Id: I04e75759dbd48f2c8fac3fb6db825cb8ca5bc508 Reviewed-on: https://gerrit.instructure.com/135984 Tested-by: Jenkins Reviewed-by: Felix Milea-Ciobanu <fmileaciobanu@instructure.com> Reviewed-by: Steven Burnett <sburnett@instructure.com> Product-Review: Matt Goodwin <mattg@instructure.com> QA-Review: Venk Natarajan <vnatarajan@instructure.com>
This commit is contained in:
parent
98abf7b4a6
commit
23029c74cf
|
@ -18,12 +18,13 @@
|
|||
define [
|
||||
"jquery",
|
||||
"i18n!context.roster_user",
|
||||
"jsx/student_last_attended/index"
|
||||
"jquery.ajaxJSON",
|
||||
"jquery.instructure_misc_plugins",
|
||||
"jquery.loadingImg",
|
||||
"../../jquery.rails_flash_notifications",
|
||||
"link_enrollment"
|
||||
], ($, I18n) ->
|
||||
], ($, I18n, initLastAttended) ->
|
||||
$(document).ready ->
|
||||
$(".show_user_services_checkbox").change ->
|
||||
$.ajaxJSON $(".profile_url").attr("href"), "PUT",
|
||||
|
@ -39,7 +40,6 @@ define [
|
|||
$enrollment.find(".unconclude_enrollment_link_holder").hide()
|
||||
$enrollment.find(".completed_at_holder").hide()
|
||||
|
||||
|
||||
$(".conclude_enrollment_link").click (event) ->
|
||||
event.preventDefault()
|
||||
$(this).parents(".enrollment").confirmDelete
|
||||
|
@ -82,4 +82,4 @@ define [
|
|||
$(".more_user_information").slideDown()
|
||||
$(this).hide()
|
||||
|
||||
|
||||
initLastAttended(document.getElementById("student_last_attended__component"), ENV.COURSE_ID, ENV.USER_ID, ENV.LAST_ATTENDED_DATE)
|
||||
|
|
|
@ -263,6 +263,9 @@ class ContextController < ApplicationController
|
|||
@membership = scope.first
|
||||
if @membership
|
||||
@enrollments = scope.to_a
|
||||
js_env(COURSE_ID: @context.id,
|
||||
USER_ID: user_id,
|
||||
LAST_ATTENDED_DATE: @enrollments.first.last_attended_at)
|
||||
log_asset_access(@membership, "roster", "roster")
|
||||
end
|
||||
elsif @context.is_a?(Group)
|
||||
|
|
|
@ -738,6 +738,26 @@ class EnrollmentsApiController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
# @API Adds last attended date to student enrollment in course
|
||||
#
|
||||
# @example_request
|
||||
# curl https://<canvas>/api/v1/courses/:course_id/user/:user_id/last_attended"
|
||||
# -X PUT => date="Thu%20Dec%2021%202017%2000:00:00%20GMT-0700%20(MST)
|
||||
#
|
||||
#
|
||||
# @returns Enrollment
|
||||
def last_attended
|
||||
return unless authorized_action(@context, @current_user, [:view_all_grades, :manage_grades])
|
||||
date = Time.zone.parse(params[:date])
|
||||
if date
|
||||
enrollments = Enrollment.where(:course_id => params[:course_id], :user_id => params[:user_id])
|
||||
enrollments.update_all(last_attended_at: date)
|
||||
render :json => {:date => date}
|
||||
else
|
||||
render :json => { :message => 'Invalid date time input' }, :status => :bad_request
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
# Internal: Collect course enrollments that @current_user has permissions to
|
||||
# read.
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* Copyright (C) 2017 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import moment from 'moment'
|
||||
import axios from 'axios'
|
||||
import I18n from 'i18n!last_attended'
|
||||
|
||||
import Container from '@instructure/ui-core/lib/components/Container'
|
||||
import DateInput from '@instructure/ui-core/lib/components/DateInput'
|
||||
import Text from '@instructure/ui-core/lib/components/Text'
|
||||
import Spinner from '@instructure/ui-core/lib/components/Spinner'
|
||||
import ScreenReaderContent from '@instructure/ui-core/lib/components/ScreenReaderContent'
|
||||
|
||||
import {showFlashError} from '../shared/FlashAlert'
|
||||
|
||||
export default class StudentLastAttended extends React.Component {
|
||||
static propTypes = {
|
||||
defaultDate: PropTypes.string,
|
||||
courseID: PropTypes.number.isRequired,
|
||||
studentID: PropTypes.number.isRequired
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
defaultDate: null
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
const currentDate = new Date(moment(this.props.defaultDate).toString())
|
||||
this.state = {
|
||||
selectedDate: currentDate || null,
|
||||
messages: [],
|
||||
loading: false
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.createCancelToken()
|
||||
}
|
||||
|
||||
onDateSubmit = (e, date) => {
|
||||
const currentDate = new Date(date)
|
||||
const messages = this.checkDateValidations(currentDate)
|
||||
if (!messages.length) {
|
||||
this.postDateToBackend(currentDate)
|
||||
} else {
|
||||
this.setState({messages})
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnMount() {
|
||||
this.source.cancel()
|
||||
}
|
||||
|
||||
// Used to allow us to cancel the axios call when posting date
|
||||
createCancelToken() {
|
||||
const cancelToken = axios.CancelToken
|
||||
this.source = cancelToken.source()
|
||||
}
|
||||
|
||||
checkDateValidations(date) {
|
||||
if (date.toString() === 'Invalid Date') {
|
||||
return [{text: I18n.t('Enter a valid date'), type: 'error'}]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
postDateToBackend(currentDate) {
|
||||
this.setState({loading: true})
|
||||
axios
|
||||
.put(`/api/v1/courses/${this.props.courseID}/users/${this.props.studentID}/last_attended`, {
|
||||
date: currentDate,
|
||||
cancelToken: this.source.token
|
||||
})
|
||||
.then(() => {
|
||||
this.setState({loading: false, selectedDate: currentDate})
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({loading: false})
|
||||
showFlashError(I18n.t('Failed To Change Last Attended Date'))
|
||||
})
|
||||
}
|
||||
|
||||
renderTitle() {
|
||||
return (
|
||||
<Container display="block" margin="small 0">
|
||||
<Text margin="small 0">{I18n.t('Last day attended')}</Text>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.loading) {
|
||||
return (
|
||||
<Container display="block" margin="small x-small">
|
||||
{this.renderTitle()}
|
||||
<Container display="block" margin="small">
|
||||
<Spinner
|
||||
margin="small 0"
|
||||
display="block"
|
||||
title={I18n.t('Loading last attended date')}
|
||||
size="small"
|
||||
/>
|
||||
</Container>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Container display="block" margin="small x-small">
|
||||
{this.renderTitle()}
|
||||
<DateInput
|
||||
previousLabel={I18n.t('Previous Month')}
|
||||
nextLabel={I18n.t('Next Month')}
|
||||
label={<ScreenReaderContent>{I18n.t('Set Last Attended Date')}</ScreenReaderContent>}
|
||||
onDateChange={this.onDateSubmit}
|
||||
messages={this.state.messages}
|
||||
dateValue={
|
||||
!this.state.selectedDate || this.state.selectedDate.toString() === 'Invalid Date'
|
||||
? null
|
||||
: this.state.selectedDate.toISOString()
|
||||
}
|
||||
validationFeedback={false}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright (C) 2018 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import '@instructure/ui-themes/lib/canvas'
|
||||
import React from 'react'
|
||||
import { mount, shallow } from 'enzyme'
|
||||
import StudentLastAttended from '../StudentLastAttended'
|
||||
|
||||
const defaultProps = () => ({
|
||||
defaultDate: "2018-03-04T07:00:00.000Z",
|
||||
courseID: 1,
|
||||
studentID: 1,
|
||||
})
|
||||
|
||||
test('renders the StudentLastAttended component', () => {
|
||||
const tree = mount(<StudentLastAttended {...defaultProps()} />)
|
||||
expect(tree.exists()).toBe(true)
|
||||
})
|
||||
|
||||
test('renders loading component when loading', () => {
|
||||
const tree = shallow(<StudentLastAttended {...defaultProps()} />)
|
||||
tree.setState({ loading: true})
|
||||
const node = tree.find('Spinner')
|
||||
expect(node).toHaveLength(1)
|
||||
})
|
||||
|
||||
test('onDateSubmit calls correct function', () => {
|
||||
const tree = mount(<StudentLastAttended {...defaultProps()} />)
|
||||
const node = tree.find('Text')
|
||||
expect(node.text()).toBe('Last day attended')
|
||||
})
|
||||
|
||||
test('onDateSubmit sets correct error for invalid date', () => {
|
||||
const tree = mount(<StudentLastAttended {...defaultProps()} />)
|
||||
const instance = tree.instance();
|
||||
instance.onDateSubmit({}, 'Invalid Date')
|
||||
expect(tree.state('messages')[0].type).toBe('error')
|
||||
})
|
||||
|
||||
test('checkDateValidations returns no messages for real date', () => {
|
||||
const tree = mount(<StudentLastAttended {...defaultProps()} />)
|
||||
const instance = tree.instance();
|
||||
expect(instance.checkDateValidations('2018-03-04T07:00:00.000Z')).toEqual([])
|
||||
})
|
||||
|
||||
test('checkDateValidations returns error messages for invalid date', () => {
|
||||
const tree = mount(<StudentLastAttended {...defaultProps()} />)
|
||||
const instance = tree.instance();
|
||||
expect(instance.checkDateValidations('Invalid Date')[0].type).toBe('error')
|
||||
})
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (C) 2018 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import StudentLastAttended from './StudentLastAttended'
|
||||
|
||||
export default function initLastAttended(rootElement, courseID, studentID, lastAttendedDate) {
|
||||
ReactDOM.render(
|
||||
<StudentLastAttended
|
||||
defaultDate={lastAttendedDate}
|
||||
courseID={courseID}
|
||||
studentID={studentID}
|
||||
/>,
|
||||
rootElement
|
||||
)
|
||||
}
|
|
@ -198,6 +198,9 @@
|
|||
</tr>
|
||||
<% end %>
|
||||
</table>
|
||||
<% if @context.is_a?(Course) && can_do(@context, @current_user, :manage_admin_users) %>
|
||||
<div id="student_last_attended__component"></div>
|
||||
<% end %>
|
||||
</fieldset>
|
||||
<%= render :partial => 'courses/link_enrollment' %>
|
||||
<% end %>
|
||||
|
|
|
@ -987,6 +987,7 @@ CanvasRails::Application.routes.draw do
|
|||
post 'courses/:course_id/enrollments/:id/accept', action: :accept
|
||||
post 'courses/:course_id/enrollments/:id/reject', action: :reject
|
||||
|
||||
put 'courses/:course_id/users/:user_id/last_attended', :action => :last_attended
|
||||
put 'courses/:course_id/enrollments/:id/reactivate', :action => :reactivate, :as => 'reactivate_enrollment'
|
||||
|
||||
delete 'courses/:course_id/enrollments/:id', action: :destroy, :as => "destroy_enrollment"
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
#
|
||||
# Copyright (C) 2017 - present Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, version 3 of the License.
|
||||
#
|
||||
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
class AddLastAttendedAtToEnrollments < ActiveRecord::Migration[5.0]
|
||||
tag :predeploy
|
||||
def change
|
||||
add_column :enrollments, :last_attended_at, :datetime
|
||||
end
|
||||
end
|
|
@ -178,12 +178,14 @@ describe ContextController do
|
|||
expect(flash[:error]).to be_present
|
||||
end
|
||||
|
||||
it 'limits enrollments by visibility' do
|
||||
it 'limits enrollments by visibility for course default section' do
|
||||
user_session(@student)
|
||||
get 'roster_user', params: {:course_id => @course.id, :id => @teacher.id}
|
||||
expect(response).to be_success
|
||||
expect(assigns[:enrollments].map(&:course_section_id)).to match_array([@course.default_section.id, @other_section.id])
|
||||
end
|
||||
|
||||
it 'limits enrollments by visibility for other section' do
|
||||
user_session(@other_student)
|
||||
get 'roster_user', params: {:course_id => @course.id, :id => @teacher.id}
|
||||
expect(response).to be_success
|
||||
|
|
Loading…
Reference in New Issue