dsr tool for canvas
Change-Id: I6211433cb17abcf981908885b9c6db8cc7b33284 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/351433 Reviewed-by: Jason Perry <jason.perry@instructure.com> Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> QA-Review: Alex Slaughter <aslaughter@instructure.com> Product-Review: Alex Slaughter <aslaughter@instructure.com> Build-Review: Alex Slaughter <aslaughter@instructure.com>
This commit is contained in:
parent
9880d540ec
commit
82f7e81ce9
|
@ -1699,6 +1699,12 @@ class AccountsController < ApplicationController
|
|||
redirect_to course_url(params[:id])
|
||||
end
|
||||
|
||||
def can_create_dsr
|
||||
Feature.definitions["enable_dsr_requests"] &&
|
||||
@account.root_account&.feature_enabled?(:enable_dsr_requests) &&
|
||||
@account.grants_any_right?(@current_user, session, :manage_dsr_requests)
|
||||
end
|
||||
|
||||
def course_user_search
|
||||
return unless authorized_action(@account, @current_user, :read)
|
||||
|
||||
|
@ -1723,6 +1729,7 @@ class AccountsController < ApplicationController
|
|||
js_permissions = {
|
||||
can_read_course_list:,
|
||||
can_read_roster:,
|
||||
can_create_dsr:,
|
||||
can_create_courses: @account.grants_any_right?(@current_user, session, :manage_courses, :create_courses),
|
||||
can_create_users: @account.root_account.grants_right?(@current_user, session, :manage_user_logins),
|
||||
analytics: @account.service_enabled?(:analytics),
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
We've finished creating a copy of the Canvas data you requested on June 26, 2024. You can download your files until August 1, 2024.
|
||||
Your download will contain data from Canvas LMS and should be treated with care.
|
||||
|
||||
Follow the link below to download your export
|
||||
<%= @data[:download_url] %>
|
|
@ -0,0 +1,9 @@
|
|||
<p>
|
||||
We've finished creating a copy of the Canvas data you requested on June 26, 2024. You can download your files until August 1, 2024.
|
||||
Your download will contain data from Canvas LMS and should be treated with care.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Follow the link below to download your export
|
||||
<b><%= @data[:download_url] %></b>
|
||||
</p>
|
|
@ -294,9 +294,23 @@ class CommunicationChannel < ActiveRecord::Base
|
|||
!raw_number.start_with?(Login::OtpHelper::DEFAULT_US_COUNTRY_CODE)
|
||||
end
|
||||
|
||||
def send_dsr_notification!(dsr_request)
|
||||
account = dsr_request.account
|
||||
download_url = dsr_request.access_url
|
||||
|
||||
m = messages.temp_record
|
||||
m.to = path
|
||||
m.context = account || Account.default
|
||||
m.user = user
|
||||
m.notification = Notification.new(name: "dsr_request", category: "Registration")
|
||||
m.data = { download_url: }
|
||||
m.parse!("email")
|
||||
m.subject = "Canvas DSR Code"
|
||||
Mailer.deliver(Mailer.create_message(m))
|
||||
end
|
||||
|
||||
def send_otp!(code, account = nil)
|
||||
message = t :body, "Your Canvas verification code is %{verification_code}", verification_code: code
|
||||
|
||||
case path_type
|
||||
when TYPE_SMS
|
||||
if Setting.get("mfa_via_sms", true) == "true" && e164_path && account&.feature_enabled?(:notification_service)
|
||||
|
|
|
@ -19,19 +19,21 @@
|
|||
#
|
||||
|
||||
class Progress < ActiveRecord::Base
|
||||
belongs_to :context, polymorphic:
|
||||
[:content_migration,
|
||||
:course,
|
||||
:account,
|
||||
:group_category,
|
||||
:content_export,
|
||||
:assignment,
|
||||
:attachment,
|
||||
:epub_export,
|
||||
:sis_batch,
|
||||
:course_pace,
|
||||
:context_external_tool,
|
||||
{ context_user: "User", quiz_statistics: "Quizzes::QuizStatistics" }]
|
||||
belongs_to :context, polymorphic: [
|
||||
:content_migration,
|
||||
:course,
|
||||
:account,
|
||||
:group_category,
|
||||
:content_export,
|
||||
:assignment,
|
||||
:attachment,
|
||||
:epub_export,
|
||||
:sis_batch,
|
||||
:course_pace,
|
||||
:context_external_tool,
|
||||
{ context_user: "User", quiz_statistics: "Quizzes::QuizStatistics" },
|
||||
] + (defined?(DsrRequest) ? [:dsr_request] : [])
|
||||
|
||||
belongs_to :user
|
||||
belongs_to :delayed_job, class_name: "::Delayed::Job", optional: true
|
||||
|
||||
|
|
|
@ -320,6 +320,18 @@ Rails.application.config.to_prepare do
|
|||
"AccountAdmin"
|
||||
]
|
||||
},
|
||||
manage_dsr_requests: {
|
||||
label: -> { I18n.t("permissions.manage_dsr_requests", "Create DSR Exports for Users") },
|
||||
label_v2: -> { I18n.t("Users - create DSR export") },
|
||||
available_to: [
|
||||
"AccountAdmin",
|
||||
"AccountMembership"
|
||||
],
|
||||
account_only: :root,
|
||||
true_for: [
|
||||
"AccountAdmin"
|
||||
]
|
||||
},
|
||||
manage_user_observers: {
|
||||
label: -> { I18n.t("permissions.manage_user_observers", "Manage observers for users") },
|
||||
label_v2: -> { I18n.t("Users - manage observers") },
|
||||
|
|
|
@ -306,13 +306,11 @@ colorized rails log and a browser screenshot taken at the time of the failure.
|
|||
## Extra Services
|
||||
|
||||
### Mail Catcher
|
||||
Mail Catcher is used to both send and view email in a development environment.
|
||||
|
||||
To enable Mail Catcher: Add `docker-compose/mailcatcher.override.yml` to your `COMPOSE_FILE` var in `.env`.
|
||||
To enable Mail Catcher: Add `docker-compose/mailcatcher.override.yml` to your `COMPOSE_FILE` var in `.env`. Then you can `docker compose up mailcatcher`.
|
||||
|
||||
Email is often sent through background jobs if you spin up the `jobs` container.
|
||||
If you would like to test or preview any notifications, simply trigger the email
|
||||
through its normal actions, and it should immediately show up in the emulated
|
||||
webmail inbox available here: http://mail.canvas.docker/
|
||||
Email is often sent through background jobs in the jobs container. If you would like to test or preview any notifications, simply trigger the email through its normal actions, and it should immediately show up in the emulated webmail inbox available here: <http://mail.canvas.docker>
|
||||
|
||||
### Canvas RCE API
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
# See doc/docker/README.md or https://github.com/instructure/canvas-lms/tree/master/doc/docker
|
||||
version: '2.3'
|
||||
services:
|
||||
web: &WEB
|
||||
build:
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# to use this add docker-compose/mailcatcher.override.yml
|
||||
# to your COMPOSE_FILE var in .env
|
||||
|
||||
version: '2.3'
|
||||
services:
|
||||
web:
|
||||
links:
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
# to use this add docker-compose/pgweb.override.yml
|
||||
# to your COMPOSE_FILE var in .env
|
||||
|
||||
services:
|
||||
web:
|
||||
links:
|
||||
- pgweb
|
||||
|
||||
pgweb:
|
||||
image: sosedoff/pgweb:latest
|
||||
command: [
|
||||
/usr/bin/pgweb, --bind=0.0.0.0, --ssl=disable, --db=canvas_development,
|
||||
--host=postgres, --user=postgres, --pass=sekret
|
||||
]
|
||||
environment:
|
||||
VIRTUAL_HOST: pgweb.canvas.docker
|
||||
links:
|
||||
- postgres
|
|
@ -1,7 +1,6 @@
|
|||
# to use this add docker-compose/rce-api.override.yml
|
||||
# to your COMPOSE_FILE var in .env
|
||||
|
||||
version: '2.3'
|
||||
services:
|
||||
web:
|
||||
links:
|
||||
|
|
|
@ -246,6 +246,7 @@ colorized rails log and a browser screenshot taken at the time of the failure.
|
|||
## Extra Services
|
||||
|
||||
### Mail Catcher
|
||||
Mail Catcher is used to both send and view email in a development environment.
|
||||
|
||||
To enable Mail Catcher: Add `docker-compose/mailcatcher.override.yml` to your `COMPOSE_FILE` var in `.env`. Then you can `docker compose up mailcatcher`.
|
||||
|
||||
|
|
|
@ -0,0 +1,192 @@
|
|||
/*
|
||||
* 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 $ from 'jquery'
|
||||
import React from 'react'
|
||||
import {bool, func, shape, string, element, oneOf} from 'prop-types'
|
||||
import {Button} from '@instructure/ui-buttons'
|
||||
import {Text} from '@instructure/ui-text'
|
||||
import {TextInput} from '@instructure/ui-text-input'
|
||||
import {RadioInputGroup, RadioInput} from '@instructure/ui-radio-input'
|
||||
import {FormFieldGroup} from '@instructure/ui-form-field'
|
||||
import {View} from '@instructure/ui-view'
|
||||
|
||||
import update from 'immutability-helper'
|
||||
import {get, isEmpty} from 'lodash'
|
||||
import axios from '@canvas/axios'
|
||||
|
||||
import {useScope as useI18nScope} from '@canvas/i18n'
|
||||
import preventDefault from '@canvas/util/preventDefault'
|
||||
import unflatten from 'obj-unflatten'
|
||||
import Modal from '@canvas/instui-bindings/react/InstuiModal'
|
||||
|
||||
const I18n = useI18nScope('account_course_user_search')
|
||||
|
||||
const trim = (str = '') => str.trim()
|
||||
|
||||
const initialState = {
|
||||
open: false,
|
||||
data: {
|
||||
request_name: null,
|
||||
},
|
||||
errors: {},
|
||||
}
|
||||
|
||||
export default class CreateDSRModal extends React.Component {
|
||||
static propTypes = {
|
||||
// whatever you pass as the child, when clicked, will open the dialog
|
||||
children: element.isRequired,
|
||||
url: string.isRequired,
|
||||
user: shape({
|
||||
name: string.isRequired,
|
||||
sortable_name: string,
|
||||
short_name: string,
|
||||
email: string,
|
||||
time_zone: string,
|
||||
}),
|
||||
customized_login_handle_name: string,
|
||||
delegated_authentication: bool,
|
||||
showSIS: bool,
|
||||
afterSave: func.isRequired,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
customized_login_handle_name: window.ENV.customized_login_handle_name,
|
||||
delegated_authentication: window.ENV.delegated_authentication,
|
||||
showSIS: window.ENV.SHOW_SIS_ID_IN_NEW_USER_FORM,
|
||||
}
|
||||
|
||||
state = {...initialState}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState(update(this.state, {data: {
|
||||
$set: {
|
||||
request_name: ENV.ROOT_ACCOUNT_NAME.toString().replace(/\s+/g, '-') + '-' + (new Date).toISOString().split('T')[0],
|
||||
request_output: "xlsx",
|
||||
}
|
||||
}}))
|
||||
}
|
||||
|
||||
onChange = (field, value) => {
|
||||
this.setState(prevState => {
|
||||
let newState = update(prevState, {
|
||||
data: unflatten({[field]: {$set: value}}),
|
||||
errors: {$set: {}},
|
||||
})
|
||||
return newState
|
||||
})
|
||||
}
|
||||
|
||||
close = () => this.setState({open: false})
|
||||
|
||||
onSubmit = () => {
|
||||
if (!isEmpty(this.state.errors)) return
|
||||
const method = 'POST'
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
axios({url: this.props.url, method, data: this.state.data}).then(
|
||||
response => {
|
||||
const dsr_request = response.data
|
||||
const request_name = dsr_request.request_name
|
||||
$.flashMessage(
|
||||
I18n.t(
|
||||
'DSR Request *%{request_name}* was created successfully! You will receive an email upon completion.',
|
||||
{request_name}
|
||||
)
|
||||
)
|
||||
|
||||
this.setState({...initialState})
|
||||
if (this.props.afterSave) this.props.afterSave(response)
|
||||
},
|
||||
({response}) => {
|
||||
$.flashError('Something went wrong creating the DSR request.')
|
||||
this.setState({errors: {
|
||||
request_name: ["Invalid request name"]
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
render = () => (
|
||||
<span>
|
||||
<Modal
|
||||
as="form"
|
||||
onSubmit={preventDefault(this.onSubmit)}
|
||||
open={this.state.open}
|
||||
onDismiss={this.close}
|
||||
size="medium"
|
||||
label={
|
||||
I18n.t('Create DSR Request')
|
||||
}
|
||||
>
|
||||
<Modal.Body>
|
||||
<FormFieldGroup layout="stacked" rowSpacing="small" description="">
|
||||
<TextInput
|
||||
key="request_name"
|
||||
renderLabel={<>
|
||||
{I18n.t('DSR Request Name')} <Text color="danger"> *</Text>
|
||||
</>}
|
||||
label={ I18n.t('DSR Request Name')}
|
||||
data-testid={ I18n.t('DSR Request Name') }
|
||||
value={get(this.state.data, "request_name")?.toString() ?? ''}
|
||||
onChange={e =>
|
||||
this.onChange("request_name", e.target.value)
|
||||
}
|
||||
isRequired={true}
|
||||
layout="inline"
|
||||
messages={(this.state.errors["request_name"] || [])
|
||||
.map(errMsg => ({type: 'error', text: errMsg}))
|
||||
.concat({type: 'hint', text: I18n.t('This is a a common tracking ID for DSR requests.')})
|
||||
.filter(Boolean)}
|
||||
/>
|
||||
<View as="div" padding="0 0 0 medium">
|
||||
<RadioInputGroup
|
||||
name="request_output"
|
||||
description="Output Format"
|
||||
layout="columns"
|
||||
value={get(this.state.data, "request_output")?.toString() ?? ''}
|
||||
onChange={e =>
|
||||
this.onChange("request_output", e.target.value)
|
||||
}
|
||||
>
|
||||
<RadioInput value="xlsx" label="Excel" />
|
||||
{/* Enabled once we agree on a format for PDF */}
|
||||
{/* <RadioInput value="pdf" label="PDF" /> */}
|
||||
</RadioInputGroup>
|
||||
</View>
|
||||
</FormFieldGroup>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={this.close}>{I18n.t('Cancel')}</Button>
|
||||
<Button type="submit" color="primary">
|
||||
{ I18n.t('Create') }
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
{React.Children.map(this.props.children, child =>
|
||||
// when you click whatever is the child element to this, open the modal
|
||||
React.cloneElement(child, {
|
||||
onClick: (...args) => {
|
||||
if (child.props.onClick) child.props.onClick(...args)
|
||||
this.setState({open: true})
|
||||
},
|
||||
})
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
|
@ -21,10 +21,11 @@ import {arrayOf, func, object, shape, string} from 'prop-types'
|
|||
import {IconButton} from '@instructure/ui-buttons'
|
||||
import {Table} from '@instructure/ui-table'
|
||||
import {Tooltip} from '@instructure/ui-tooltip'
|
||||
import {IconEditLine, IconMasqueradeLine, IconMessageLine} from '@instructure/ui-icons'
|
||||
import {IconEditLine, IconMasqueradeLine, IconMessageLine, IconExportLine} from '@instructure/ui-icons'
|
||||
import {useScope as useI18nScope} from '@canvas/i18n'
|
||||
import FriendlyDatetime from '@canvas/datetime/react/components/FriendlyDatetime'
|
||||
import CreateOrUpdateUserModal from './CreateOrUpdateUserModal'
|
||||
import CreateDSRModal from './CreateDSRModal'
|
||||
import UserLink from './UserLink'
|
||||
import TempEnrollUsersListRow from '@canvas/temporary-enrollment/react/TempEnrollUsersListRow'
|
||||
|
||||
|
@ -117,6 +118,29 @@ export default function UsersListRow({
|
|||
</span>
|
||||
</CreateOrUpdateUserModal>
|
||||
)}
|
||||
{permissions.can_create_dsr && (
|
||||
<CreateDSRModal
|
||||
url={`/api/v1/accounts/${accountId}/users/${user.id}/dsr_request`}
|
||||
user={user}
|
||||
afterSave={handleSubmitEditUserForm}
|
||||
>
|
||||
<span>
|
||||
<Tooltip
|
||||
data-testid="user-list-row-tooltip"
|
||||
renderTip={I18n.t('Create DSR Request for %{name}', {name: user.name})}
|
||||
>
|
||||
<IconButton
|
||||
withBorder={false}
|
||||
withBackground={false}
|
||||
size="small"
|
||||
screenReaderLabel={I18n.t('Create DSR Request for %{name}', {name: user.name})}
|
||||
>
|
||||
<IconExportLine title={I18n.t('Create DSR Request for %{name}', {name: user.name})} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</CreateDSRModal>
|
||||
)}
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue