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:
Alex Slaughter 2024-06-27 10:27:57 +02:00
parent 9880d540ec
commit 82f7e81ce9
14 changed files with 302 additions and 23 deletions

View File

@ -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),

View File

@ -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] %>

View File

@ -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>

View File

@ -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)

View File

@ -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

View File

@ -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") },

View File

@ -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

View File

@ -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:

View File

@ -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:

View File

@ -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

View File

@ -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:

View File

@ -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`.

View File

@ -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> &nbsp;
<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>
)
}

View File

@ -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>
)